#coding=utf-8

import sqlite3
import shelve
import tempfile
import os
import shutil
import pickle
import time
import heapq
import locale
import re
import sys
import zlib
import gzip
import uuid
import glob
import datetime
from multiprocessing import Process
from multiprocessing import Queue
import queue
from collections import deque

from PyQt4.QtGui import *
from PyQt4.QtCore import *
from PyQt4.QtWebKit import *

import lonote.translate as translate
import lonote.path as path
import lonote.editor as editor
import lonote.status as status
import lonote.dba as dba
import lonote.treeview as treeview
import lonote.cfgdialog as cfgdialog
import lonote.html as html
import lonote.filelock as filelock
import lonote.google_dmp.diff_match_patch as diff_match_patch
import lonote.version as version
import lonote.subprocess as subprocess
import lonote.info as info
import lonote.timestr as timestr
import lonote.shuext as shuext
import lonote.dialog as dialog
import lonote.js as js

#初始化全域物件
dmp = diff_match_patch()

class LoMain(QMainWindow):
    '''放在LoMain中的頁面'''

#初始化與關閉

    def __init__(self, parent = None, status = None, app = None):
        '''不做任何設定，只是初始化介面佈局'''
        super().__init__(parent)
        #預設的物件
        self.listView = treeview.LoTreeView(self)
        self.searchView = treeview.LoTreeView(self)
        self.searchBar = QLineEdit(self)
        self.searchNumber = QLabel(self)
        self.treeView = treeview.LoTreeView(self)
        self.editor = editor.LoEditor(self)
        self.hPreviewScreen = editor.LoPerviewer(self)
        self.hPreviewTitle = QLabel(self)
        self.hPreviewAccept = QPushButton(self)
        self.hPreviewCancel = QPushButton(self)
        self.hPreviewModeHtml = QPushButton(self)
        self.hPreviewID = None #記錄目前被preview的ID
        self.hPreviewModtime = None #記錄目前被preview的Modtime
        self.hPreviewHTML = '' #記錄目前被preview的HTML原始碼
        self.hPreviewDiff = '' #記錄目前被preview的Diff（以Html表示）
        self.hPreviewScrollposHTML = {} #以id為索引的字典
        self.hPreviewScrollposPlain = {}

        self.attachView = QListView(self)
        self.attachAdd = QPushButton(self)
        self.historyView = QTreeWidget(self)
        self.historyAdd = QPushButton(self)
        self.historyClear = QPushButton(self)
        self.headingView = QTreeWidget(self)
        self.bookmarkView = treeview.LoBookmarkView(self)
        self.bookmarkAdd = QPushButton(self)
        self.bookmarkDelete = QPushButton(self)
        self.bookmarkClear = QPushButton(self)
        self.toolbar = QToolBar(self)
        self.notebookMenu = QMenu(self)
        self.saveTimer = QTimer(self)
        self.renewTimer = QTimer(self)
        self.innerTitleBar = QWidget(self)
        self.innerTitle = QLabel(self)
        self.innerTitleCharcount = QLabel(self)
        self.cDialog = cfgdialog.LoCfgDialog(self)
        self.setWindowIcon(QIcon(path.icons.lonote))
        self.tabup = QTabWidget(self)
        self.conn = None
        self.bhistory = None
        self.status = status
        self.bookLock = None #筆記簿檔案鎖
        self.ezpage = QWebPage() #方便的暫存頁面
        self.asyncQueue = Queue() #非同步的資料
        #set，用來確認特定頁面的contentsChanged存在或否
        self.ccSignalExist = None

        #設定總事件過濾器
        if app:
            app.installEventFilter(self)

        #數值初始化
        self._initAction()
        self._initLayout()
        self._initTray()
        self._fixConnect()
        self.statusBar()
        self._loadStatus()

    def _fixConnect(self):
        '''把該建立的固定Connect建好'''
        self.treeView.itemPressed.connect(self._treeClicked)
        self.treeView.SIGNALcommitData.connect(self._renameTree)
        self.treeView.SIGNALdropData.connect(self._treeAfterDrop)
        self.listView.itemPressed.connect(self._listClicked)
        self.listView.SIGNALcommitData.connect(self._renameTree)
        self.attachView.doubleClicked.connect(self._attachDoubleClicked)
        self.attachAdd.pressed.connect(self._attachAddClicked)
        self.searchView.itemPressed.connect(self._searchClicked)
        self.searchView.SIGNALcommitData.connect(self._renameTree)
        self.searchBar.textChanged.connect(self._searchInput)
        self.bookmarkView.SIGNALdropData.connect(self._bookmarkAfterDrop)
        self.bookmarkView.itemPressed.connect(self._searchClicked)
        self.bookmarkView.SIGNALcommitData.connect(self._renameTree)
        self.bookmarkAdd.pressed.connect(self._bookmarkAddClicked)
        self.bookmarkDelete.pressed.connect(self._bookmarkDeleteClicked)
        self.bookmarkClear.pressed.connect(self._bookmarkClearClicked)
        self.historyView.itemPressed.connect(self._historyClicked)
        self.historyAdd.pressed.connect(self._historyAddClicked)
        self.historyClear.pressed.connect(self._historyClearClicked)
        self.headingView.itemPressed.connect(self._headingClicked)
        self.editor.lowikiClickedSignal.connect(self.lowikiClicked)
        self.hPreviewAccept.pressed.connect(self._hPreviewAcceptClicked)
        self.hPreviewCancel.pressed.connect(self._hPreviewCancelClicked)
        self.hPreviewModeHtml.toggled.connect(self._hPreviewModeHtmlToggled)
        self.saveTimer.timeout.connect(self.autoSave)
        self.renewTimer.timeout.connect(self.autoRenew)

    def _setDisplay(self):
        '''從status讀入顯示狀況，並加以套用'''
        if self.status['WINDOW_WIDTH'] > 0 and self.status['WINDOW_HEIGHT'] > 0:
            self.resize(self.status['WINDOW_WIDTH'], self.status['WINDOW_HEIGHT'])
        #如果值為None則不設定
        if self.status['RIGHT_SPLITTER_SIZES']:
            self.rightSplitter.setSizes(self.status['RIGHT_SPLITTER_SIZES'])
        if self.status['MAIN_SPLITTER_SIZES']:
            self.mainSplitter.setSizes(self.status['MAIN_SPLITTER_SIZES'])
        if self.status['RD_SPLITTER_SIZES']:
            self.rdSplitter.setSizes(self.status['RD_SPLITTER_SIZES'])

        if self.status['WINDOW_MAXIMIZED']:
            self.showMaximized()
        self.tableft.setCurrentIndex(self.status['LEFTTAB_POS'])
        #設定hPreviewModeHtml按鈕點下去與否
        self.hPreviewModeHtml.setChecked(self.status['HISTORY_PREVIEW_HTMLMODE'])

    def _initAction(self):
        '''靜態的Action在此設定'''
        #筆記簿按鈕：notebook
        self._noteBook = QAction(QIcon(path.icons.notebook), _('Notebook'), self)
        self._noteBook.setStatusTip(_('The notebook\'s operation!'))
        self._noteBook.triggered.connect(self.noteBook)
        #建立新筆記頁:createNewNote
        self._newNote = QAction(QIcon(path.icons.newnote), _('New page'), self)
        self._newNote.setShortcut('Ctrl+N')
        self._newNote.setStatusTip(_('Create a new page (Ctrl+N)'))
        self._newNote.triggered.connect(self.newNote)
        #建立子筆記頁:
        self._newSubNote = QAction(QIcon(path.icons.newsubnote), _('New sub page'), self)
        self._newSubNote.setShortcut('Ctrl+Alt+N')
        self._newSubNote.setStatusTip(_('Create a new sub page (Ctrl+Alt+N)'))
        self._newSubNote.triggered.connect(self.newSubNote)
        #刪除筆記頁:deleteNote
        self._delNote = QAction(QIcon(path.icons.delnote), _('Delete page'), self)
        self._delNote.setShortcut('Alt+del')
        self._delNote.setStatusTip(_('Delete current note page (Alt+del)'))
        self._delNote.triggered.connect(self.delNote)
        #新建新筆記簿:newNoteBook
        self._newNoteBook = QAction(QIcon(path.icons.newnotebook), _('New notebook'), self)
        self._newNoteBook.setStatusTip(_('Take out a new notebook!'))
        self._newNoteBook.triggered.connect(self.newNoteBook)
        self._newNoteBook.setIconVisibleInMenu(True)
        #讀取一份筆記:loadnote
        self._loadNoteBook = QAction(QIcon(path.icons.loadnotebook), _('Open notebook'), self)
        self._loadNoteBook.setStatusTip(_('Open an existent notebook!'))
        self._loadNoteBook.triggered.connect(self.loadNoteBook)
        self._loadNoteBook.setIconVisibleInMenu(True)
        #關於:about
        self._about = QAction(QIcon(path.icons.aboutbutton), _('About & info'), self)
        self._about.setShortcut('F1')
        self._about.setStatusTip(_('About LoNote (F1)'))
        self._about.triggered.connect(self.about)
        #設定視窗:cfgDialog
        self._cfgDialog = QAction(QIcon(path.icons.cfgdialog), _('Config'), self)
        self._cfgDialog.setShortcut('F5')
        self._cfgDialog.setStatusTip(_('Open configure dialog (F5)'))
        self._cfgDialog.triggered.connect(self.cfgDialog)
        #快速匯出Html
        self._fastExportToHtml = QAction(QIcon(path.icons.export), _('Fast Export (HTML)'), self)
        self._fastExportToHtml.setStatusTip(_('Export current page as HTML (F6)    ☆Hint: you can find your files in "NOTEBOOK_PATH/export"'))
        self._fastExportToHtml.triggered.connect(self.fastExportToHtml)
        self._fastExportToHtml.setIconVisibleInMenu(True)
        #快速匯出Htmls為網站
        self._exportToHtmls = QAction(QIcon(path.icons.export), _('Export (HTML Website)'), self)
        self._exportToHtmls.setStatusTip(_('Export current page and subpage as HTML website    ☆Hint: you can find your files in "NOTEBOOK_PATH/export"'))
        self._exportToHtmls.triggered.connect(self.exportToHtmls)
        self._exportToHtmls.setIconVisibleInMenu(True)
        #匯出成新筆記簿
        self._exportToNewNotebook = QAction(QIcon(path.icons.export), _('Export as a new Notebook'), self)
        self._exportToNewNotebook.setStatusTip(_('Export current page and all subpage as a new notebook.    ☆Hint: you can find your new book in "NOTEBOOK_PATH/export"'))
        self._exportToNewNotebook.triggered.connect(self.exportToNewNotebook)
        self._exportToNewNotebook.setIconVisibleInMenu(True)
        #從筆記簿匯入
        self._importFromNotebook = QAction(QIcon(path.icons.export), _('import from a Notebook'), self)
        self._importFromNotebook.setStatusTip(_('import a notebook as subpage.'))
        self._importFromNotebook.triggered.connect(self.importFromNotebook)
        self._importFromNotebook.setIconVisibleInMenu(True)
        #跳到上層筆記頁:
        self._upperNote = QAction('↖', self)
        self._upperNote.setStatusTip(_('Jump to upper page (F10)'))
        self._upperNote.triggered.connect(self.upperNote)
        #跳到上一頁
        self._beforeNote = QAction('↶', self)
        self._beforeNote.setStatusTip(_('Jump to before page (F11)'))
        self._beforeNote.triggered.connect(self.beforeNote)
        #跳到下一頁
        self._nextNote = QAction('↷', self)
        self._nextNote.setStatusTip(_('Jump to next page (F12)'))
        self._nextNote.triggered.connect(self.nextNote)

    def _initLayout(self):
        '''把面板拼好'''
        #拼裝面板
        searchBarLayout = QHBoxLayout()
        searchBarLayout.addWidget(self.searchNumber)
        searchBarLayout.addWidget(self.searchBar)

        upLayout = QVBoxLayout()
        upLayout.addLayout(searchBarLayout)
        #upLayout.setContentsMargins(4,4,4,4)
        upLayout.addWidget(self.searchView)
        upWidget = QWidget(self)
        upWidget.setLayout(upLayout)
        self.tabup.addTab(self.listView, _('Viewer'))
        self.tabup.addTab(upWidget, _('Search'))
        self.tabup.currentChanged.connect(self._tabupChanged)

        historyLayout = QVBoxLayout()
        historyLayout.setContentsMargins(4,4,4,4)
        historyLayout.setSpacing(4)
        historyLayout.addWidget(self.historyAdd)
        historyLayout.addWidget(self.historyView)
        historyLayout.addWidget(self.historyClear)
        historyWidget = QWidget()
        historyWidget.setLayout(historyLayout)

        attachLayout = QVBoxLayout()
        attachLayout.setContentsMargins(4,4,4,4)
        attachLayout.setSpacing(4)
        attachLayout.addWidget(self.attachAdd)
        attachLayout.addWidget(self.attachView)
        attachWidget = QWidget()
        attachWidget.setLayout(attachLayout)

        tabdown = QTabWidget()
        tabdown.setUsesScrollButtons(False)
        tabdown.addTab(self.headingView, _('TOC'))
        tabdown.addTab(historyWidget, _('Hist.'))
        tabdown.addTab(attachWidget, _('Atta.'))

        hPreviewBarLayout = QHBoxLayout()
        hPreviewBarLayout.addWidget(self.hPreviewTitle)
        hPreviewBarLayout.addStretch()
        hPreviewBarLayout.addWidget(self.hPreviewAccept)
        hPreviewBarLayout.addWidget(self.hPreviewCancel)
        hPreviewBarLayout.addWidget(self.hPreviewModeHtml)

        hPreviewLayout = QVBoxLayout()
        hPreviewLayout.addLayout(hPreviewBarLayout)
        hPreviewLayout.addWidget(self.hPreviewScreen)

        self.hPreview = QWidget()
        self.hPreview.hide()
        self.hPreview.setLayout(hPreviewLayout)

        viewerLayout = QVBoxLayout()
        viewerLayout.setContentsMargins(0,0,0,0)
        viewerLayout.setSpacing(0)
        viewerLayout.addWidget(self.editor)
        viewerLayout.addWidget(self.hPreview)
        viewer = QWidget()
        viewer.setLayout(viewerLayout)

        self.rdSplitter = QSplitter(self)
        self.rdSplitter.addWidget(viewer)
        self.rdSplitter.addWidget(tabdown)

        innerTitleToolBar = QToolBar(self)
        innerTitleToolBar.addAction(self._upperNote)
        innerTitleToolBar.addAction(self._beforeNote)
        innerTitleToolBar.addAction(self._nextNote)
        innerTitleBarLayout = QHBoxLayout()
        innerTitleBarLayout.setContentsMargins(0,0,20,0)
        innerTitleBarLayout.setSpacing(0)
        innerTitleBarLayout.addWidget(innerTitleToolBar)
        innerTitleBarLayout.addWidget(self.innerTitle)
        innerTitleBarLayout.addStretch()
        innerTitleBarLayout.addWidget(self.innerTitleCharcount)
        self.innerTitleBar.setLayout(innerTitleBarLayout)

        lay = QVBoxLayout()
        lay.setContentsMargins(0,0,0,0)
        lay.setSpacing(0)
        lay.addWidget(self.innerTitleBar)
        lay.addWidget(self.rdSplitter)
        lay.setStretchFactor(self.rdSplitter, True)
        wid = QWidget()
        wid.setLayout(lay)

        self.rightSplitter = QSplitter(self)
        self.rightSplitter.setOrientation(Qt.Vertical)
        self.rightSplitter.addWidget(self.tabup)
        self.rightSplitter.addWidget(wid)

        leftLayout = QVBoxLayout()
        self.tableft = QTabWidget(self)
        self.tableft.setTabPosition(QTabWidget.North)
        self.tableft.setUsesScrollButtons(False)
        #self.tableft.setStyleSheet('QTabWidget::tab-bar { alignment : right }')
        bookmarkLayout = QVBoxLayout()
        bookmarkLayout.setContentsMargins(4,4,4,4)
        bookmarkLayout.setSpacing(4)
        bookmarkButtonLayout = QHBoxLayout()
        bookmarkButtonLayout.setContentsMargins(0,0,0,0)
        bookmarkButtonLayout.addWidget(self.bookmarkDelete)
        bookmarkButtonLayout.addWidget(self.bookmarkAdd)
        bookmarkLayout.addLayout(bookmarkButtonLayout)
        bookmarkLayout.addWidget(self.bookmarkView)
        bookmarkLayout.addWidget(self.bookmarkClear)
        bookmarkWidget = QWidget()
        bookmarkWidget.setLayout(bookmarkLayout)

        self.tableft.addTab(self.treeView, _('Viewer'))
        self.tableft.addTab(bookmarkWidget, _('Bookmarks'))

        leftLayout.addWidget(self.toolbar)
        leftLayout.addWidget(self.tableft)

        leftWidget = QWidget(self)
        leftWidget.setLayout(leftLayout)

        self.mainSplitter = QSplitter(self)
        self.mainSplitter.addWidget(leftWidget)
        self.mainSplitter.addWidget(self.rightSplitter)
        self.setCentralWidget(self.mainSplitter)

        #開始進行細節設定
        #hPreview按鈕細節
        self.hPreviewAccept.setText(_('Accept'))
        self.hPreviewAccept.setStatusTip(_('Changes to the selected version!'))
        self.hPreviewCancel.setText(_('Cancel'))
        self.hPreviewCancel.setStatusTip(_('Won\'t do anything.'))
        self.hPreviewModeHtml.setText(_('Preview'))
        self.hPreviewModeHtml.setStatusTip(_('Checking what this version look like...'))
        self.hPreviewModeHtml.setCheckable(True)

        #self.hPreviewTitle.setWordWrap(True)

        #設定bookmarkView細節
        self.bookmarkView.setHeaderHidden(True)
        self.bookmarkView.setColumnCount(3)
        self.bookmarkView.header().setSectionHidden(1,True)
        self.bookmarkView.header().setSectionHidden(2,True)
        self.bookmarkView.setAcceptDrops(True)
        self.bookmarkView.setDragEnabled(True)

        #設定bookmark按鈕細節
        self.bookmarkAdd.setText(_('Add'))
        self.bookmarkAdd.setStatusTip(_('Add a note into the bookmarks list. (Alt+=)'))
        self.bookmarkDelete.setText(_('Remove'))
        self.bookmarkDelete.setStatusTip(_('Remove a note from the bookmarks list. (Alt+-)'))
        self.bookmarkClear.setText(_('Clear All!'))
        self.bookmarkClear.setStatusTip(_('Remove all notes from the bookmarks list.'))

        #設定tabup面板細節
        self.tabup.setTabPosition(QTabWidget.East)
        self.tabup.setUsesScrollButtons(False)

        #設定list面板細節
        self.listView.setHeaderHidden(False)

        head = QTreeWidgetItem()
        head.setText(0,_('Name'))
        head.setText(1,'id')
        head.setText(2,'order')
        head.setText(3,_('Modified time'))
        head.setText(4,_('Created time'))
        head.setText(5,'modtime')
        head.setText(6,'cretime')
        head.setText(7,_('Chars'))
        head.setText(8,'len')
        self.listView.setHeaderItem(head)

        self.listView.setColumnCount(9)
        self.listView.header().setSectionHidden(1,True)
        self.listView.header().setSectionHidden(2,True)
        self.listView.header().setSectionHidden(3,False)
        self.listView.header().setSectionHidden(4,False)
        self.listView.header().setSectionHidden(5,True)
        self.listView.header().setSectionHidden(6,True)
        self.listView.header().setSectionHidden(7,False)
        self.listView.header().setSectionHidden(8,True)
        self.listView.header().setMovable(False) #section不能被搬動
        self.listView.setItemDelegate(treeview.EditorDelegate(self.listView))

        #設定searchNumber標籤細節
        self.searchNumber.setTextFormat(Qt.RichText)
        self.searchNumber.setFixedWidth(80)
        self.searchNumber.setAlignment(Qt.AlignCenter)
        self.searchNumber.setText('>>>>')

        #設定searchBar細節
        self.searchBar.setStatusTip(_('Input some keyword for search!    ☆Hint: You can use "title:", "attachment:", "modified:", "created:" prefix too.'))

        #設定search面板細節
        head = QTreeWidgetItem()
        head.setText(0,_('Name'))
        head.setText(1,'id')
        head.setText(2,'order')
        head.setText(3,_('Modified time'))
        head.setText(4,_('Created time'))
        head.setText(5,'modtime')
        head.setText(6,'cretime')
        head.setText(7,_('Chars'))
        head.setText(8,'len')
        self.searchView.setHeaderItem(head)

        self.searchView.setColumnCount(9)
        self.searchView.header().setSectionHidden(1,True)
        self.searchView.header().setSectionHidden(2,True)
        self.searchView.header().setSectionHidden(3,False)
        self.searchView.header().setSectionHidden(4,False)
        self.searchView.header().setSectionHidden(5,True)
        self.searchView.header().setSectionHidden(6,True)
        self.searchView.header().setSectionHidden(7,False)
        self.searchView.header().setSectionHidden(8,True)
        self.searchView.header().setMovable(False) #section不能被搬動

        #設定tree面板細節
        self.treeView.setHeaderHidden(True)
        self.treeView.setColumnCount(3)
        self.treeView.header().setSectionHidden(1,True)
        self.treeView.header().setSectionHidden(2,True)
        self.treeView.setAcceptDrops(True)
        self.treeView.setDragEnabled(True)

        #設定history面板細節
        head = QTreeWidgetItem()
        head.setText(0,_('Recorded time'))
        head.setText(1,_('Name'))
        head.setText(2,'modtime')
        head.setText(3,_('Chars'))
        self.historyView.setHeaderItem(head)

        self.historyView.setColumnCount(4)
        self.historyView.header().setSectionHidden(1,True)
        self.historyView.header().setSectionHidden(2,True)
        self.historyView.header().setSectionHidden(3,False)
        self.historyView.header().setMovable(False) #section不能被搬動

        #設定History按鈕細節
        self.historyAdd.setText(_('Add'))
        self.historyAdd.setStatusTip(_('Add current version into history list.'))
        self.historyClear.setText(_('Clear All!'))
        self.historyClear.setStatusTip(_('Remove all history of this page.'))

        #設定Attach按鈕細節
        self.attachAdd.setText(_('Add'))
        self.attachAdd.setStatusTip(_('Add a attachment in current page.'))
        self.attachAdd.setIcon(QIcon(path.icons.inatt))

        #設定InnerTitle細節
        self.innerTitleBar.setContentsMargins(0,0,0,0)
        self.innerTitle.setTextFormat(Qt.RichText)
        #self.innerTitle.setIndent(20)
        #self.innerTitle.setMaximumHeight(30)
        #self.innerTitleCharcount.setMaximumWidth(100)

        #設定heading面板細節
        self.headingView.setHeaderHidden(True)
        self.headingView.setColumnCount(2)
        self.headingView.header().setSectionHidden(0, False)
        self.headingView.header().setSectionHidden(0, True)

        #擺放toolbar Action
        self.toolbar.setIconSize(QSize(32,32))
        self.toolbar.setLayoutDirection(Qt.RightToLeft)
        self.toolbar.addAction(self._newNote)
        self.toolbar.addAction(self._newSubNote)
        self.toolbar.addSeparator()
        self.toolbar.addAction(self._noteBook)
        self.toolbar.addSeparator()
        self.toolbar.addAction(self._delNote)
        self.toolbar.addSeparator()
        self.toolbar.addAction(self._cfgDialog)
        self.toolbar.addAction(self._about)

    def _initTray(self):
        '''初始化系統托盤'''

        #拼裝tray menu
        menu = QMenu(self)
        #displayAction
        displayAction = QAction(QIcon(path.icons.notebook), _('Show / Hide'), menu)
        displayAction.triggered.connect(self.displayChange)
        menu.addAction(displayAction)
        #aboutAction
        menu.addAction(self._about)
        menu.addSeparator()
        #closeAction
        closeAction = QAction(QIcon(path.icons.notebook), _('Close'), menu)
        closeAction.triggered.connect(self.close)
        menu.addAction(closeAction)
        #搭建tray icon
        self.trayIcon = QSystemTrayIcon(self)
        self.trayIcon.setToolTip(_('LoNote Personal Notebook'))
        self.trayIcon.setIcon(QIcon(path.icons.lonote))
        self.trayIcon.activated.connect(self._trayActivated)
        self.trayIcon.setContextMenu(menu)
        self.trayIcon.show()

    def closeEvent(self, event):
        '''關閉事件'''
        #記錄視窗狀態
        self.status['RIGHT_SPLITTER_SIZES'] = self.rightSplitter.sizes()
        self.status['MAIN_SPLITTER_SIZES'] = self.mainSplitter.sizes()
        self.status['RD_SPLITTER_SIZES'] = self.rdSplitter.sizes()

        self.status['WINDOW_MAXIMIZED'] = self.isMaximized()
        self.status['LEFTTAB_POS'] = self.tableft.currentIndex()

        if not self.isMaximized():
            #只有在沒有Maximized時才記錄size
            self.status['WINDOW_WIDTH'] = self.size().width()
            self.status['WINDOW_HEIGHT'] = self.size().height()

        #記錄hPreview的模式
        self.status['HISTORY_PREVIEW_HTMLMODE'] = self.hPreviewModeHtml.isChecked()
        #關閉筆記
        self.closeBook()
        #存入檔案
        self.cDialog.close()
        self.status.sync()
        self.status.close()

#針對使用者對程式的（全域的）操作處理

    def eventFilter(self, obj, event):
        '''事件過濾'''
        if event.type() == QEvent.MouseButtonPress or event.type() == QEvent.MouseButtonDblClick:
            if event.button() == Qt.XButton1: #上一頁
                self.beforeNote()
                return True
            elif event.button() == Qt.XButton2: #下一頁
                self.nextNote()
                return True
        return False

    def keyPressEvent(self, event):
        '''鍵盤敲擊時的事件'''
        if event.key() == Qt.Key_F11: #上一頁按鈕
            self.beforeNote()
        elif event.key() == Qt.Key_F12: #下一頁按鈕
            self.nextNote()
        elif event.key() == Qt.Key_F10: #F10前往上層
            self.upperNote()
        elif event.modifiers() == Qt.ControlModifier and (event.key() == Qt.Key_Slash or event.key() == Qt.Key_Backslash):
            self.searchHotKeyPressed()

        elif event.key() == Qt.Key_Down: #在lineEditor中按「下」
            if self.searchBar.hasFocus():
                self.searchView.setFocus()
                #選擇它
                self.searchView.setCurrentIndex(self.searchView.currentIndex())
        elif event.key() == Qt.Key_Return:
            if self.searchView.hasFocus(): #在SearchView中按「Enter」
                self._searchClickedByReturn(self.searchView.currentItem(),1)
            elif self.listView.hasFocus(): #在listView中按「Enter」
                self._listClickedByReturn(self.listView.currentItem(),1)
            elif self.bookmarkView.hasFocus(): #在bookmarkView中按「Enter」
                self._bookmarkClickedByReturn(self.bookmarkView.currentItem(),1)
            elif self.treeView.hasFocus(): #在treeView中按「Enter」
                self._treeClickedByReturn(self.treeView.currentItem(),1)
            elif self.searchBar.hasFocus(): #在lineEditor中按「Enter」
                self.searchView.setFocus()
                #選擇它
                self.searchView.setCurrentIndex(self.searchView.currentIndex())
        elif event.key() == Qt.Key_Escape: #按Esc，無條件跳到編輯區
            self.editor.noteArea.setFocus()
            self.allSelect()
        elif event.modifiers() == Qt.AltModifier and event.key() == Qt.Key_Equal: #Alt + =，加入書籤
            self._bookmarkAddClicked()
        elif event.modifiers() == Qt.AltModifier and event.key() == Qt.Key_Minus: #Alt + -，移除書籤
            self._bookmarkDeleteClicked()
        elif event.modifiers() == Qt.AltModifier and event.key() == Qt.Key_B: #Alt + B，切換書籤面板
            if self.bookmarkView.hasFocus():
                self.tableft.setCurrentIndex(0)
            else:
                self.tableft.setCurrentIndex(1)
                self.bookmarkView.setFocus()
        elif event.key() == Qt.Key_F6: #F6快速匯出
            self.fastExportToHtml()
        super().keyPressEvent(event)

    def _trayActivated(self, reason):
        '''tray被觸發時的操作'''
        if reason == QSystemTrayIcon.Trigger:
            self.displayChange()

    def displayChange(self):
        '''在顯示與隱藏之間切換'''
        if self.isVisible() == True:
            if self.isActiveWindow():
                self.setVisible(False)
            else:
                self.activateWindow()
                self.raise_()
        else:
            self.setVisible(True)
            self.activateWindow()
            self.raise_()

#Status操作

    def _loadStatus(self):
        '''從lonoterc中設定狀態'''
        if self.status == None:
            self.status = shelve.open(filename = path.fix.cfg, protocol = 3, writeback = True)
        self._checkStatus()

        self.setAsStatus()
        self._setDisplay()
        self.loadBook()

    def _checkStatus(self):
        '''檢查status是否存在，不存在的話使用預設值'''
        #檢查值的存在，如果沒有值就使用預設值
        #執行參數檢查
        try: self.status['MULTIPROCESSING']
        except KeyError:
            if sys.platform.startswith('win'):
                self.status['MULTIPROCESSING'] = False
            else:
                self.status['MULTIPROCESSING'] = False

        #視窗顯示狀態檢查
        try: self.status['RIGHT_SPLITTER_SIZES']
        except KeyError: self.status['RIGHT_SPLITTER_SIZES'] = None
        try: self.status['MAIN_SPLITTER_SIZES']
        except KeyError: self.status['MAIN_SPLITTER_SIZES'] = None
        try: self.status['RD_SPLITTER_SIZES']
        except KeyError: self.status['RD_SPLITTER_SIZES'] = [1,0]

        try: self.status['WINDOW_WIDTH']
        except KeyError: self.status['WINDOW_WIDTH'] = 0
        try: self.status['WINDOW_HEIGHT']
        except KeyError: self.status['WINDOW_HEIGHT'] = 0
        try: self.status['WINDOW_MAXIMIZED']
        except KeyError: self.status['WINDOW_MAXIMIZED'] = False
        try: self.status['LEFTTAB_POS'] #決定left tab當前翻在哪一面
        except KeyError: self.status['LEFTTAB_POS'] = 0

        #內部顯示狀態檢查
        try: self.status['SHOW_INNERTITLE']
        except KeyError: self.status['SHOW_INNERTITLE'] = False
        try: self.status['INNERTITLE_FORMAT']
        except KeyError: self.status['INNERTITLE_FORMAT'] = '<p style="white-space: pre">        <i><b><font size="4"> 『 {$NOTENAME} 』 </font><font size="1" color="gray">↙ {$PARENTNAME}</font></b></i></p>'
        try: self.status['TRACE_SCROLLPOS']
        except KeyError: self.status['TRACE_SCROLLPOS'] = True
        try: self.status['DONT_REFRESH_LISTVIEW']
        except KeyError: self.status['DONT_REFRESH_LISTVIEW'] = False
        try: self.status['USE_CSS']
        except KeyError: self.status['USE_CSS'] = True
        try: self.status['CSS_FILE']
        except KeyError: self.status['CSS_FILE'] = 'light.css'
        try: self.status['EDITOR_FONT_ST']
        except KeyError: self.status['EDITOR_FONT_ST'] = ''
        try: self.status['EDITOR_TEXT_SIZE_MULTIPLIER']
        except KeyError: self.status['EDITOR_TEXT_SIZE_MULTIPLIER'] = 1.0
        try: self.status['SHOW_CHARCOUNT']
        except KeyError: self.status['SHOW_CHARCOUNT'] = False
        try: self.status['SHOW_INSTANT_CHARCOUNT']
        except KeyError: self.status['SHOW_INSTANT_CHARCOUNT'] = False

        #時間格式設定檢查
        try: self.status['TIMEFORMAT_CREATED']
        except KeyError: self.status['TIMEFORMAT_CREATED'] = '%x'
        try: self.status['TIMEFORMAT_MODIFIED']
        except KeyError: self.status['TIMEFORMAT_MODIFIED'] = '%x'
        try: self.status['TIMEFORMAT_HISTORY']
        except KeyError: self.status['TIMEFORMAT_HISTORY'] = '%b%d %H:%M:%S'
        try: self.status['INSERTFORMAT_TIME']
        except KeyError: self.status['INSERTFORMAT_TIME'] = '%X'
        try: self.status['INSERTFORMAT_DATE']
        except KeyError: self.status['INSERTFORMAT_DATE'] = '%x'

        #圖片插入參數檢查
        try: self.status['INSERTFORMAT_IMAGE']
        except KeyError: self.status['INSERTFORMAT_IMAGE'] = '<a href="{$FILENAME}">{$IMAGE}</a>'
        try: self.status['INSERTFORMAT_IMAGE_WIDTH']
        except KeyError: self.status['INSERTFORMAT_IMAGE_WIDTH'] = 500
        try: self.status['INSERTFORMAT_IMAGE_SRC_RESIZE_ENABLE']
        except KeyError: self.status['INSERTFORMAT_IMAGE_SRC_RESIZE_ENABLE'] = True
        try: self.status['INSERTFORMAT_IMAGE_SRC_RESIZE_QUALITY']
        except KeyError: self.status['INSERTFORMAT_IMAGE_SRC_RESIZE_QUALITY'] = -1

        #筆記本讀取訊息檢查
        try: self.status['ISEXAMPLE']
        except KeyError: self.status['ISEXAMPLE'] = True
        try: self.status['BOOKDIR']
        except KeyError: self.status['BOOKDIR'] = '$DEFAULT$'
        try: self.status['NOTEBOOK_HISTORY']
        except KeyError: self.status['NOTEBOOK_HISTORY'] = deque([],maxlen = 10)

        #版本記錄參數檢查
        try: self.status['VCS_ENABLE']
        except KeyError: self.status['VCS_ENABLE'] = True
        try: self.status['AUTOBACKUP_PERIOD'] #單位為秒……預設周期為三天
        except KeyError: self.status['AUTOBACKUP_PERIOD'] = 60*60*24*3
        try: self.status['AUTOBACKUP_LENCHANGE'] #變更字數，超過就儲存版本
        except KeyError: self.status['AUTOBACKUP_LENCHANGE'] = 200
        try: self.status['AUTOBACKUP_LEVENSHTEIN'] #使用複雜字數計算法
        except: self.status['AUTOBACKUP_LEVENSHTEIN'] = False
        try: self.status['HISTORY_PREVIEW_HTMLMODE'] #設定hPreview的模式
        except KeyError: self.status['HISTORY_PREVIEW_HTMLMODE'] = False

        #資料參數檢查
        try: self.status['AUTOREMOVE_UNUSED_EMBEDDED_FILES']
        except KeyError: self.status['AUTOREMOVE_UNUSED_EMBEDDED_FILES'] = False
        try: self.status['AUTOSAVE_PERIOD'] #自動儲存周期（秒）
        except KeyError: self.status['AUTOSAVE_PERIOD'] = 30

        #匯出參數檢查
        try: self.status['OPEN_AFTER_EXPORT_TO_HTML']
        except KeyError: self.status['OPEN_AFTER_EXPORT_TO_HTML'] = True
        try: self.status['EXPORT_TO_HTML_WHEN_CLOSE']
        except KeyError: self.status['EXPORT_TO_HTML_WHEN_CLOSE'] = False
        try: self.status['EXPORT_EDITOR_FONT']
        except KeyError: self.status['EXPORT_EDITOR_FONT'] = False

    def setAsStatus(self):
        '''將LoMain用當前的Status狀態更新
        但Display不動，也不載入筆記'''
        #更新編輯器格式
        self.editor.DATE_FORMAT = self.status['INSERTFORMAT_DATE']
        self.editor.TIME_FORMAT = self.status['INSERTFORMAT_TIME']
        self.editor.IMG_FORMAT = self.status['INSERTFORMAT_IMAGE']
        self.editor.IMG_FORMAT_WIDTH = self.status['INSERTFORMAT_IMAGE_WIDTH']
        self.editor.IMG_FORMAT_INSERTRESIZE_ENABLE = self.status['INSERTFORMAT_IMAGE_SRC_RESIZE_ENABLE']
        self.editor.IMG_FORMAT_INSERTRESIZE_QUALITY = self.status['INSERTFORMAT_IMAGE_SRC_RESIZE_QUALITY']
        #更新編輯器當前頁面的CSS
        if self.status['USE_CSS']:
            self.editor.csspath = path.cal.notecss(self.status['BOOKDIR'], self.editor.getID(), self.status['CSS_FILE'])
        else:
            self.editor.csspath = ''
        #更新編輯器字型
        self.editor.fontfamily_st = self.status['EDITOR_FONT_ST']
        #更新編輯器字體大小乘數
        self.editor.fontsize_multiplier = self.status['EDITOR_TEXT_SIZE_MULTIPLIER']
        #設定innerTitle
        self.innerTitleBar.setVisible(self.status['SHOW_INNERTITLE'])
        self._setMainName()
        #設定即時字數計算
        self.currentCharcount()
        #設定字數顯示狀態
        listHeader = self.listView.header()
        listHeader.setSectionHidden(7,not self.status['SHOW_CHARCOUNT'])
        self.resizeHeader(listHeader)
        searchHeader = self.searchView.header()
        searchHeader.setSectionHidden(7,not self.status['SHOW_CHARCOUNT'])
        self.resizeHeader(searchHeader)
        #設定自動儲存計時器
        if self.status['AUTOSAVE_PERIOD'] <= 0:
            self.saveTimer.stop()
        else:
            self.saveTimer.start(self.status['AUTOSAVE_PERIOD'] * 1000)
        #設定自動更新計時器
        self.renewTimer.start(250)

#筆記本操作

    def loadBook(self):
        '''從status中讀出指定的Notebook
        如果isexample == True，則複製筆記範本並開啟她'''
        def exampleLocaleDir():
            '''取得範例的本地化目錄'''
            localeWord = translate.loadLocaleSetting()
            localeWord = translate.getRealLocaleWord(localeWord)

            localeExampleDir = os.path.join(path.exampleDir, localeWord)
            if not os.path.isdir(localeExampleDir):
                localeExampleDir = os.path.join(path.exampleDir, 'nolocale')
            return localeExampleDir

        #如果路徑不存在，則讀取EXAMPLE。
        if not os.path.isfile(path.cal.bookdb(self.status['BOOKDIR'])):
            self.status['ISEXAMPLE'] = True

        #如果要讀取EXAMPLE，套用BOOKDIR並製作副本
        if self.status['ISEXAMPLE']:
            example = exampleLocaleDir()
            name = tempfile.mkdtemp(prefix = 'lonote_example_')
            self.status['BOOKDIR'] = os.path.join(name, 'example')
            shutil.copytree(example ,self.status['BOOKDIR'])

        #測試資料庫版本：版本比程式使用得還新就讀取example
        version = dba.getVersion(path.cal.bookdb(self.status['BOOKDIR']))
        #這是當前所使用的資料庫版本，大於這個版本的資料庫不該被本程式處理（小於的可以升級上來）
        if version > dba.currentNotebookVersion:
            dialog.infoDialog(_('LoNote need update'),_('"...???... I don\'t understand how to open this book!"\n\nThis notebook using newer format, you need to update your LoNote then open it again.\n\nBook:\n"{}"').format(self.status['BOOKDIR']),path.icons.questiondialog)
            self.status['ISEXAMPLE'] = True
            self.loadBook()
            return

        #測試檔案鎖狀態
        self.bookLock = filelock.FileLock(path.cal.booklock(self.status['BOOKDIR']))
        try: self.bookLock.acquire()
        except filelock.acquireException:
            answer = dialog.questionDialog(_('The notebook already open!'),_('''"You can't use it yet..."\n\nThe notebook:\n"{0}"\nwas opened by another LoNote.\n\nContinue load it?\n\nIf you want force to use it. Click "Yes".\n\nOtherwise LoNote will rollback your previous notebook. Please click "No" and waiting...''').format(self.status['BOOKDIR']),path.icons.error)
            if answer == False: #選No則回滾
                for notebook in self.status['NOTEBOOK_HISTORY']:
                    #再次進行可用性測試
                    try:
                        tmplock = filelock.FileLock(path.cal.booklock(notebook[1]))
                        tmplock.acquire()
                        #通過可用性測試才能繼續執行下面內容
                        tmplock.release()
                        self.status['ISEXAMPLE'] = False
                        self.status['BOOKDIR'] = notebook[1]
                        self.loadBook()
                        return
                    except filelock.acquireException:
                        tmplock.release()
                #如果把歷史紀錄都跑光了，還是找不到，只好讀取筆記範本了
                self.status['ISEXAMPLE'] = True
                self.loadBook()
                return
            else: #刪除檔案鎖並執行
                if os.path.isfile(path.cal.booklock(self.status['BOOKDIR'])):
                    os.remove(path.cal.booklock(self.status['BOOKDIR']))
                    print(_('You\'re already delete the filelock.'))

        #已確認路徑可用

        #如果不是EXAMPLE，進行資料庫檢測
        if not self.status['ISEXAMPLE']:
            #檢查資料庫狀態
            conn = sqlite3.connect(path.cal.bookdb(self.status['BOOKDIR']))
            reports = conn.execute('PRAGMA integrity_check;').fetchall()
            conn.close()
            if reports[0][0] != 'ok': #如果出了錯誤
                print('------------')
                print(_('[DB Check]:'), self.status['BOOKDIR'])
                for report in reports:
                    print(report[0])
                #有錯則禁止讀取他，以免進一步破壞
                print(_('Database Error, stop loading...'))
                self.status['ISEXAMPLE'] = True
                self.loadBook()
                return
            #如果資料庫有錯，這時應該允許使用者復原備份的檔案
            #（但備份功能尚未寫出，因此暫時保留）
            elif self.status['MULTIPROCESSING']:
                #沒出錯且使用多進程模式時，進行備份
                bookdb = path.cal.bookdb(self.status['BOOKDIR'])
                backupdir = path.cal.backupdir(self.status['BOOKDIR'])
                if not os.path.isdir(backupdir): #如有必要建立備份路徑
                    os.mkdir(backupdir)
                #解算備份檔案檔名
                filename = str(datetime.date.today())
                backupFilePath = os.path.join(backupdir, filename)
                full_backupFilePath = backupFilePath + '.zip'
                #如果今日的備份檔尚未建立，則建立一份
                if not os.path.isfile(full_backupFilePath):
                    #建立壓縮備份檔
                    tempdir = os.path.join(self.status['BOOKDIR'], filename)
                    tempfilepos = os.path.join(tempdir, os.path.basename(bookdb))
                    os.mkdir(tempdir)
                    shutil.move(bookdb, tempfilepos)
                    shutil.make_archive( #實際建立
                            backupFilePath, 'zip',
                            root_dir = tempdir)
                    #將剛剛搬移的資料復位
                    shutil.move(tempfilepos, bookdb)
                    os.rmdir(tempdir)

        #實際讀取
        self.conn = sqlite3.connect(
                path.cal.bookdb(self.status['BOOKDIR']))

        #現在確定讀到了，而且也可以正常使用
        #如果有必要，試著去升級他。
        dba.checkUpgrade(path.cal.bookdb(self.status['BOOKDIR']))

        #進行資料庫性能優化與其他設定
        #self.conn.execute('PRAGMA locking_mode = EXCLUSIVE;')
        self.conn.execute('PRAGMA temp_store = MEMORY;')
        self.conn.execute('PRAGMA journal_mode = WAL;')
        self.conn.execute('PRAGMA cache_size = 60000;')

        #設定編輯器值
        self.editor.dbcur = self.conn.cursor()

        #設定特殊用途筆記本層級參數
        self.ccSignalExist = set()

        #開始載入吧……
        self.genTreeView()
        #取得視野
        try:
            with open(path.cal.bhistory(self.status['BOOKDIR']), 'rb') as f:
                self.bhistory = pickle.load(f)
        except IOError:
            self.bhistory = status.bhistory()
        #進行讀取記錄
        if self.status['DONT_REFRESH_LISTVIEW']:
            self.genListView(1)
        self.changeBrowsing(self.bhistory.current())
        self._setMainName()
        self.genBookmarkView()
        self.recordNotebookHistory()
        self.genNotebookMenu()

    def closeBook(self):
        '''關閉現在的筆記簿'''
        def saveExpanded(item):
            id = item.text(1)
            self.conn.execute('update notes set tree_expanded = ? where id = ?', (item.isExpanded(), id))
            for i in range(item.childCount()):
                saveExpanded(item.child(i))

        #儲存tree折疊狀態
        treeRoot = self.treeView.topLevelItem(0)
        saveExpanded(treeRoot)
        self.conn.commit()
        #儲存list折疊狀態
        p = self.saveListExpandedAsync()
        if p:
            p.join()
        #儲存當前頁面
        p = self.saveCurrentPageAsync(False)
        if p:
            p.join()
        #回收當前頁面的失連圖檔
        self.autoRecycleImageFile(self.editor.getID())
        #儲存筆記簿瀏覽歷史
        self.recordNotebookHistory()
        #如果要求關閉時匯出，就整本匯出
        if self.status['EXPORT_TO_HTML_WHEN_CLOSE']:
            self.exportToHtml(id = 1, recu = True)
        #清除並還原LoMain內容
        self._hPreviewCancelClicked()
        self.listView.clear()
        self.treeView.clear()
        self.historyView.clear()
        self.searchView.clear()
        self.bookmarkView.clear()
        self.searchBar.setText('')
        self.conn.commit()
        self.editor.resetCache()
        #使python>=3.2 的wal資料庫關閉後，資料庫仍能被 python 3.1平台 打開
        #但是關閉速度恐怕會變慢……
        self.conn.execute('PRAGMA journal_mode=DELETE;')
        self.conn.close()
        self.ccSignalExist = None
        self.bookLock.release()
        with open(path.cal.bhistory(self.status['BOOKDIR']), 'wb') as f:
            pickle.dump(self.bhistory, f, 3)

        #如果是exmaple，回收目錄
        if self.status['ISEXAMPLE']:
            shutil.rmtree(os.path.dirname(self.status['BOOKDIR']))

#定時維護

    def autoRenew(self):
        '''資料更新，每一段時間執行一次這個
        （主要功能是把非同步處理好的資料撈回來）'''
        try:
            while True:
                data = self.asyncQueue.get_nowait()
                #這行以下是和頁面相關的
                if data['id'] != self.editor.getID():
                    continue #id不相同的就拋棄
                #if data['type'] == 'newDiff':
                #    self.setDiffLen(data['diffLen'])
                #新的HistoryItem需要更新
                elif data['type'] == 'historyUpdate':
                    newHistoryItem = self._buildHistoryItem(
                            modtime = data['modtime'],
                            name = data['name'],
                            plainLen = data['plainLen'])
                    self.historyView.insertTopLevelItem(0, newHistoryItem)
                #這是頁面更動並儲存了的信號
                elif data['type'] == 'pageNewSave':
                    self.genHeadingViewAsync() #執行掃描動作，計算標題
                #這是頁面的heading分析好了的資料包
                elif data['type'] == 'headingItemsReady':
                    self.applyHeadingItems(data['items'])
        except queue.Empty:
            pass

    def autoSave(self):
        '''每當自動存檔計時器到達時間，就會執行一次這個……'''
        self.saveCurrentPageAsync()

#換頁操作

    def beforeNote(self):
        '''上一頁'''
        self.changeBrowsing(self.bhistory.before())

    def nextNote(self):
        '''下一頁'''
        self.changeBrowsing(self.bhistory.next())

    def upperNote(self):
        '''改變瀏覽位置到上一層'''
        st = self.bhistory.current()
        newSt = status.bstatus()
        try:
            newSt.editor = self.conn.execute('select parentid from notes where id = ?', (st.editor,)).fetchone()[0]
            if newSt.editor == None:
                newSt.editor = 1
        except: #如果無法定位出新的Editor位置，則放棄
            return
        try: newSt.treeID = self.conn.execute(
                'select parentid from notes where id = ?',
                (st.treeID,)).fetchone()[0]
        except: newSt.treeID = None
        try: newSt.listID = self.conn.execute(
                'select parentid from notes where id = ?',
                (st.listID,)).fetchone()[0]
        except: newSt.listID = None
        self.changeBrowsing(newSt)

#換頁操作（底層的）

    def changeBrowsing(self, st):
        '''改變瀏覽值
        如果當前給定的值不正確（editorID不存在），回滾到正確為止'''
        try: #測試st.editor
            self.conn.execute('select id from notes where id = ?',(st.editor,)).fetchone()[0]
        except:
            st = self.bhistory.current()
            while True:
                try: #測試st.editor
                    self.conn.execute('select id from notes where id = ?',(st.editor,)).fetchone()[0] #沒有異常就可跳出迴圈
                    break
                except:
                    st = self.bhistory.before()

        items = self.treeView.findItems(
                str(st.treeID),
                Qt.MatchFixedString | Qt.MatchRecursive,
                1)
        try:
            self.treeView.setCurrentItem(items[0])
            self.treeView.scrollToItem(items[0])
        except IndexError: pass

        if not self.status['DONT_REFRESH_LISTVIEW']:
            self.genListView(st.treeID)

        items = self.listView.findItems(
                str(st.listID),
                Qt.MatchFixedString | Qt.MatchRecursive,
                1)
        try:
            self.listView.setCurrentItem(items[0])
            self.listView.scrollToItem(items[0])
        except IndexError: pass
        if st.editor == None: #如果editor沒有值，就定位在筆記首頁
            self.changePage(1)
        else:
            self.changePage(st.editor)

        self.allSelect()

    def changePage(self, id):
        '''頁面變成id頁面'''
        #同一頁就別費事了，放棄吧~
        if id == self.editor.getID() or id == -1:
            return

        #確定要更新了！
        #保存舊頁面, 變更頁面時不強制儲存history
        self.saveCurrentPageAsync(False)
        #回收失連圖檔
        self.autoRecycleImageFile(self.editor.getID())

        #準備更新，收集資料
        #content
        content = self.conn.execute('select content from notes where id = ?', (id,)).fetchone()[0]

        #實際注入編輯器，換頁！
        self.editor.inject(htmlstr = content,
                id = id,
                embPath = path.cal.noteemb(self.status['BOOKDIR'], id))
        if self.status['USE_CSS']: #設定CSS
            self.editor.csspath = path.cal.notecss(
                    self.status['BOOKDIR'], id, self.status['CSS_FILE'])
        if self.status['TRACE_SCROLLPOS']: #設定scrollpos
            self.editor.scrollpos = self.conn.execute( 'select scrollpos from notes where id = ?', (id,)).fetchone()[0]

        #後續處理……
        #處理瀏覽記錄
        st = status.bstatus()
        try: st.treeID = int(self.treeView.currentItem().text(1))
        except AttributeError: pass
        try: st.listID = int(self.listView.currentItem().text(1))
        except AttributeError: pass
        st.editor = id
        self.bhistory.record(st)
        #更新Att檔案表
        self.genAttachView()
        #更新history檔案表
        self.saveHistoryAsync(id)
        self.genHistoryView(id)
        self._setMainName()
        #如果hPreview打開的話，把他關上
        self._hPreviewCancelClicked()
        #更新Heading表
        self.genHeadingViewAsync()
        self.editor.noteArea.clearFocus()
        self.editor.noteArea.setFocus()
        #更新字數值
        self.editorContentsChanged()
        #連接contentChanged信號（沒連接過才要連）
        #因為怎樣都無法disconnect，所以這邊用的是比較dirty的寫法
        if id not in self.ccSignalExist:
            self.editor.noteArea.page().contentsChanged.connect(self.editorContentsChanged)
            self.ccSignalExist.add(id)

#頁面保存（底層的）

    def saveCurrentPageAsync(self, forceHistoryRecord = False):
        '''非同步儲存當前頁面
        回傳process，如果運作在非同步模式下，回傳None'''
        #收集資料
        id = self.editor.getID()
        try:
            self.conn.execute('select id from notes where id = ?', (id,)).fetchone()[0]
        except:
            return False
        html = self.editor.html
        modtime = time.mktime(time.localtime())
        #記錄當前頁面捲動位置
        if self.status['TRACE_SCROLLPOS']:
            scrollpos = self.editor.scrollpos
        else:
            scrollpos = -1

        if self.status['MULTIPROCESSING']:
            p = Process(target = subprocess.savePage,
                    args = (
                        self.asyncQueue,
                        path.cal.bookdb(self.status['BOOKDIR']),
                        id,
                        html,
                        modtime,
                        scrollpos,
                        self.status['VCS_ENABLE'],
                        forceHistoryRecord,
                        self.status['AUTOBACKUP_PERIOD'],
                        self.status['AUTOBACKUP_LENCHANGE'],
                        self.status['AUTOBACKUP_LEVENSHTEIN']))
            p.start()
            return p
        else:
            subprocess.savePage(
                    asyncQueue = self.asyncQueue,
                    bookdb = path.cal.bookdb(self.status['BOOKDIR']),
                    id = id,
                    needSaveHtml = html,
                    needSaveModtime = modtime,
                    scrollpos = scrollpos,
                    vcsEnable = self.status['VCS_ENABLE'],
                    forceHistoryRecord = forceHistoryRecord,
                    autosavePeriod = self.status['AUTOBACKUP_PERIOD'],
                    autosaveLenChanged = self.status['AUTOBACKUP_LENCHANGE'],
                    useLevenshtein = self.status['AUTOBACKUP_LEVENSHTEIN'])
            return None

#特殊功能

    def autoRecycleImageFile(self, id):
        '''自動回收圖片檔案'''
        #回收已經消失的嵌入檔（圖檔）
        if self.status['AUTOREMOVE_UNUSED_EMBEDDED_FILES']:
            try:
                htmlstr = self.conn.execute('select content,id from notes where id = ?', (id,)).fetchone()[0]
            except TypeError:
                return
            isSuccess, fileList = html.getImgSrc(htmlstr)
            if isSuccess:
                #實際回收
                for dirpath, dirnames, filenames in os.walk(path.cal.noteemb(self.status['BOOKDIR'], id)):
                    for filename in filenames:
                        if not filename in fileList:
                            os.remove(os.path.join(dirpath ,filename))
                #如果有空目錄，回收目錄
                try: os.removedirs(path.cal.noteemb(self.status['BOOKDIR'], id))
                except OSError: pass
                try: os.removedirs(path.cal.noteatt(self.status['BOOKDIR'], id))
                except OSError: pass

    def recordNotebookHistory(self):
        '''在讀取後使用，記錄Notebook的讀取歷史'''
        if self.status['ISEXAMPLE']: #example的狀態下不用紀錄
            return

        bookpath = self.status['BOOKDIR']
        bookname = self.conn.execute('select name from notes where id = 1').fetchone()[0]
        item = (bookname, bookpath)
        for i in range(len(self.status['NOTEBOOK_HISTORY'])):
            #先把如果有重複的內容刪掉
            if self.status['NOTEBOOK_HISTORY'][i][1] == bookpath:
                self.status['NOTEBOOK_HISTORY'].remove(self.status['NOTEBOOK_HISTORY'][i])
                break
        #如果資料量太多，就從後端除去舊資料
        self.status['NOTEBOOK_HISTORY'].appendleft(item)

    def _setMainName(self):
        '''取得視窗現在該有的名字
        名字為筆記名加上editor的名字'''
        try:
            bookname = self.conn.execute(
                    'select name from notes where id = 1').fetchone()[0]
            notename,pID = self.conn.execute(
                    'select name,parentid from notes where id = ?',
                    (self.bhistory.current().editor,)).fetchone()
            if self.status['ISEXAMPLE']:
                self.setWindowTitle(
                        _('[Example Notebook]') +
                        ' {0}: {1}'.format(bookname, notename))
            else:
                self.setWindowTitle('{0}: {1}'.format(bookname, notename))
            #設定innerText的狀態列（顯示當前筆記路徑）
            notedir = path.cal.notedir(self.status['BOOKDIR'],
                    self.bhistory.current().editor)
            exist = ' '
            if not os.path.isdir(notedir):
                exist += _('(Not used)')
            self.innerTitle.setStatusTip(_('Current note\'s data folder: "{}"').format(notedir) + exist)

            #設定innerText
            if pID:
                pName,pID = self.conn.execute('select name,parentID from notes where id = ?', (pID,)).fetchone()
            else:
                pName = '✖'
            innerText = self.status['INNERTITLE_FORMAT'].replace('{$NOTENAME}', notename)
            innerText = innerText.replace('{$PARENTNAME}', pName)

            self.innerTitle.setText(innerText)
        except: #如果出現例外，設為預設值
            self.setWindowTitle( _('LoNote Personal Notebook'))
            self.innerTitle.setText('')

    def exportToHtml(self, id, recu = False):
        '''匯出指定頁面為HTML'''

        def genValidTitle(id):
            '''產生有效的Title'''
            title = self.conn.execute('select name from notes where id = ?', (id,)).fetchone()[0]
            title = title.translate(str.maketrans(' :*<>#?/|\\"','_：＊＜＞＃？／｜＼”'))
            return title

        def genExportContent(id, useIdFilename = False):
            '''根據id，將指定頁面要匯出為HTML時，修改後的資料回傳'''
            #收集資料
            htmlstr = self.conn.execute('select content from notes where id = ?', (id,)).fetchone()[0]
            title = genValidTitle(id)
            #準備字型設定字串
            if self.status['EXPORT_EDITOR_FONT']:
                fontCSS = '\nbody {{\nfont-family: {};\n}}\n'.format(self.status['EDITOR_FONT_ST'])
            else:
                fontCSS = ''
            #如果使用者有使用CSS，則一併匯出CSS...
            #這邊為了簡化維護直接將CSS碼寫入html檔案中
            if self.status['USE_CSS']:
                with open(path.cal.notecss(self.status['BOOKDIR'], id, self.status['CSS_FILE']), mode = 'r', encoding = 'utf-8') as file:
                    cssScript = '<style type="text/css">'  + fontCSS + file.read() + '</style>'
            else:
                cssScript = '<style type = "text/css">' + fontCSS + '</style>'
            #更新html head
            if useIdFilename:
                htmlstr = re.sub(r'<head>.*?</head>',
                        '<head><meta charset="utf-8"><base href="{embpath}"><title>{title}</title>{css}</head>'.format(title = title, embpath = str(id) + '/', css = cssScript),
                        htmlstr)
            else:
                htmlstr = re.sub(r'<head>.*?</head>',
                        '<head><meta charset="utf-8"><base href="{embpath}"><title>{title}</title>{css}</head>'.format(title = title, embpath = title + '/', css = cssScript),
                        htmlstr)
            return [id, title, htmlstr]

        def saveExportContent(id, useIdFilename = False, exportDir = ''):
            '''將指定id的資料以指定方式存放匯出'''
            id, title, htmlstr = genExportContent(id, useIdFilename = useIdFilename)
            if exportDir == '':
                exportDir = path.cal.exportdir(self.status['BOOKDIR'])
            #儲存html檔案
            if not os.path.isdir(exportDir):
                os.mkdir(exportDir)
            if useIdFilename:
                exportFile = os.path.join(exportDir, str(id) + '.html')
                targetEmb = os.path.join(exportDir, str(id))
            else:
                exportFile = os.path.join(exportDir, title + '.html')
                targetEmb = os.path.join(exportDir, title)
            with open(exportFile, mode = 'w', encoding = 'utf-8') as file:
                file.write(htmlstr)
            #複製embedded, 至於Attach就算了……
            oriEmb = path.cal.noteemb(self.status['BOOKDIR'], id)
            if os.path.isdir(oriEmb):
                if os.path.isdir(targetEmb):
                    shutil.rmtree(targetEmb)
                shutil.copytree(oriEmb, targetEmb)
            return (title, exportFile)

        def recuAllPage(id, navContent = '{$CURSOR}', level = 0, exportDir = ''):
            '''遞迴處理個別資料'''
            #收集本層資料
            title, filePath = saveExportContent(id, useIdFilename = True, exportDir = exportDir)
            localFilePath = os.path.basename(filePath)
            childNavContents = list()
            #處理下層
            for id, in self.conn.execute('select id from notes where parentid = ? order by ord asc', (id,)).fetchall():
                childFilePath, childNavContent, childTitle = recuAllPage(id, level = level + 1, exportDir = exportDir)
                childNavContents.append(childNavContent)
            #下層處理完畢，如果下層不存在
            if len(childNavContents) == 0:
                navContent = navContent.replace('{$CURSOR}', '<li>' + '<a href="{}" target="showframe">'.format(localFilePath) + title + '</a></li>{$CURSOR}')
            else: #否則把下層全塞進去……
                navContent = navContent.replace('{$CURSOR}', '<li>' + '<input type="checkbox"></input><a href="{}" target="showframe">'.format(localFilePath) + title + '</a><div><ul>{$CURSOR}')
                for cnc in childNavContents:
                    navContent = navContent.replace('{$CURSOR}', cnc)

                navContent = navContent.replace('{$CURSOR}', '</ul></div></li>{$CURSOR}')
            return (filePath, navContent, title)

        if recu:
            #解算路徑
            exportDir = path.cal.exportdir(self.status['BOOKDIR'])
            if not os.path.isdir(exportDir):
                os.mkdir(exportDir)
            subExportDir = os.path.join(exportDir, genValidTitle(id))
            #讀取navContent模板
            with open(path.cal.exportnav(self.status['BOOKDIR']), mode = 'r', encoding = 'utf-8') as file:
                navContent = file.read()
            #建立檔案
            filePath, navContent, title = recuAllPage(id ,navContent ,0 ,subExportDir)
            localFilePath = os.path.basename(filePath)
            #清掃navContent內容，把多餘的{$CURSOR}去掉
            navContent = navContent.replace('{$CURSOR}', '')
            #寫入nav
            navPath = os.path.join(subExportDir, 'nav.html')
            with open(navPath, mode = 'w', encoding = 'utf-8') as file:
                file.write(navContent)
            #寫入框架檔案
            framePath = os.path.join(exportDir, title + '.html')
            frameBody = '''<aside id="nav"><iframe id="nav" src="nav.html"></iframe></aside><div id="main"><iframe id="main" name="showframe" src="{}"></iframe></div>'''.format(localFilePath)
            frameContent = '''<!DOCTYPE html><html><head><meta charset="utf-8"><base href="{}/"></base><title>{}</title><style type="text/css">{}</style></head><body>{}</body></html>'''.format(title, title, CSS_HTMLMultiExport, frameBody)
            with open(framePath, mode = 'w', encoding = 'utf-8') as file:
                file.write(frameContent)
            #定位要開啟的檔案
            exportFile = framePath
        else:
            title, exportFile = saveExportContent(id)

        return exportFile

    def lowikiClicked(self, id):
        '''lowiki被點擊之後的行動'''
        self.changePage(id)
        self.allSelect()

    def editorContentsChanged(self):
        '''當editor內容改變時觸發這個東西'''
        self.currentCharcount()

    def currentCharcount(self):
        '''計算並顯示字數'''
        if self.status['SHOW_INNERTITLE'] and self.status['SHOW_INSTANT_CHARCOUNT']:
            charNumber = len(self.editor.plain)
            self.innerTitleCharcount.setText(
                    '<b>✔{:,}</b>'.format(charNumber))
        else:
            self.innerTitleCharcount.setText('')

#### export & import 用的特殊功能 ####

    def dataMapping(self, id, sourceBookDir, targetBookDir, idcounter = None, data = None):
        '''收集全部需要拷貝的項目對應表'''

        def recuDataMapping(id, sourceBookDir, targetBookDir, idcounter = None, data = None):
            '''遞迴處理個別資料，回傳產生的資料與idcounter'''
            #處理本層頁面
            data[id] = {
                'id':id,
                'newid':idcounter,
                'notedir':path.cal.notedir(sourceBookDir, id),
                'newnotedir':path.cal.notedir(targetBookDir, idcounter)
                }
            idcounter += 1
            #處理下層頁面
            for cID, in conn.execute('select id from notes where parentid = ?', (id,)).fetchall():
                data, idcounter = recuDataMapping(cID, sourceBookDir, targetBookDir, idcounter, data)
            #回傳data
            return data, idcounter

        #初始化
        if idcounter is None:
            if os.path.isfile(path.cal.bookdb(targetBookDir)): #檢查看看targetBookDir的id列到哪裡了
                tconn = sqlite3.connect(path.cal.bookdb(targetBookDir))
                maxid = tconn.execute('select MAX(id) from notes').fetchone()[0]
                idcounter = maxid + 1
            else: #讀不到時
                idcounter = 1
        if data is None:
            data = dict()
        conn = sqlite3.connect(path.cal.bookdb(sourceBookDir))
        #取得資料
        data, idcounter = recuDataMapping(id, sourceBookDir, targetBookDir, idcounter, data)
        #回收無用資料
        conn.close()
        return data

    def getDBLineInfo(self, sourceBookDir):
        '''取得指定筆記本的內部資料格式'''
        conn = sqlite3.connect(path.cal.bookdb(sourceBookDir))
        conn.row_factory = sqlite3.Row
        #檢測當前筆記資料項的長度，拼裝SQL語句：
        notesrow = conn.execute('select * from notes where id = 1').fetchone()
        historyrow = conn.execute('select * from history where id = 1').fetchone()
        notesLen = len(notesrow)
        historyLen = len(historyrow)
        SQL_notesInsertString = 'insert into notes values(?{})'.format(',?'*(notesLen-1))
        SQL_historyInsertString = 'insert into history values(?{})'.format(',?'*(historyLen-1))
        #檢測idPOS
        notesIDPos = notesrow.keys().index('id')
        notesPIDPos = notesrow.keys().index('parentid')
        notesOrdPos = notesrow.keys().index('ord')
        historyIDPos = historyrow.keys().index('id')
        #回收
        conn.close()
        return notesIDPos, notesPIDPos, historyIDPos, SQL_notesInsertString, SQL_historyInsertString, notesOrdPos

#View功能

## 多個View共用的功能 ##

#### View Selection位置更新 ####

    def allSelect(self):
        '''用editor的ID定位所有View的選擇狀態'''
        cID = self.bhistory.current().editor
        #定位treeView
        try:
            treeItem = self.treeView.findItems(str(cID), Qt.MatchFixedString | Qt.MatchRecursive, 1)[0]
            self.treeView.setCurrentItem(treeItem)
        except IndexError:
            self.treeView.setCurrentItem(None)
        self.allSelectWithoutTree()

    def allSelectWithoutTree(self):
        '''用editor的ID定位所有View（tree除外）的選擇狀態'''
        cID = self.bhistory.current().editor
        #定位listView
        try:
            listItem = self.listView.findItems(str(cID), Qt.MatchFixedString | Qt.MatchRecursive, 1)[0]
            self.listView.setCurrentItem(listItem)
        except IndexError:
            self.listView.setCurrentItem(None)
        #定位searchView
        try:
            searchItem = self.searchView.findItems(str(cID), Qt.MatchFixedString | Qt.MatchRecursive, 1)[0]
            self.searchView.setCurrentItem(searchItem)
        except IndexError:
            self.searchView.setCurrentItem(None)
        #定位bookmarkView
        try:
            bookmarkItem = self.bookmarkView.findItems(str(cID), Qt.MatchFixedString | Qt.MatchRecursive, 1)[0]
            self.bookmarkView.setCurrentItem(bookmarkItem)
        except IndexError:
            self.bookmarkView.setCurrentItem(None)

#### View Item展開與收縮 ####

    def _expandItem(self, item, mode = 'list'):
        '''用NoteBank裡的資料設定展開與關閉Item
        mode 可以是list或tree，決定'''
        id = int(item.text(1))
        if mode == 'list':
            expanded = bool(self.conn.execute('select list_expanded from notes where id = ?', (id,)).fetchone()[0])
        elif mode == 'tree':
            expanded = bool(self.conn.execute('select tree_expanded from notes where id = ?', (id,)).fetchone()[0])
        item.setExpanded(expanded)
        for index in range(item.childCount()):
            self._expandItem(item.child(index), mode)

#### 多個View互動 ####

    def _tabupChanged(self, index):
        '''search tab被按時，focus到searchbar上'''
        if index == 1:
            self.searchBar.setFocus()
            self.searchBar.selectAll()

#### View Item的建立與更新 ####

    def _buildTreeModel(self, twi):
        '''送入建好的QTreeWidgetItem，遞迴建立整個筆記樹'''
        data = self.conn.execute('select id,name,ord,modtime,cretime,len from notes where parentid = ?', (int(twi.text(1)),)).fetchall()
        for id,name,order,modtime,cretime,contentLen in data:
            tmp = self._buildTreeItem(name, id, order, modtime, cretime, contentLen)
            twi.addChild(tmp)
            self._buildTreeModel(tmp)

    def _buildTreeItem(self, name, id, order, modtime, cretime, contentLen):
        '''建立一個QTreeWidgetItem'''
        newItem = QTreeWidgetItem()
        self._updateTreeItem(newItem, name, id, order, modtime, cretime, contentLen)
        newItem.setFlags(Qt.ItemIsSelectable | Qt.ItemIsDropEnabled | Qt.ItemIsDragEnabled | Qt.ItemIsEnabled | Qt.ItemIsEditable)
        return newItem

    def _updateTreeItem(self, item, name = None, id = None, order = None, modtime = None, cretime = None, contentLen = None):
        '''更新一個QTreeWidgetItem'''
        if name: item.setText(0,name)
        if id: item.setText(1,str(id))
        if order != None: item.setText(2,'{:0>6}'.format(order))
        if modtime: item.setText(3,time.strftime(self.status['TIMEFORMAT_MODIFIED'], time.localtime(float(modtime))))
        if cretime: item.setText(4,time.strftime(self.status['TIMEFORMAT_CREATED'], time.localtime(float(cretime))))
        if modtime: item.setText(5,str(modtime))
        if cretime: item.setText(6,str(cretime))
        if contentLen: item.setText(7,'{:,}'.format(contentLen))
        if contentLen: item.setText(8,'{:0>15}'.format(contentLen))

    def _sortView(self, item):
        '''替指定的item排列下方順序'''
        for index in range(item.childCount()):
            child = item.child(index)
            self._sortView(child)
        item.sortChildren(2, Qt.AscendingOrder) #第3欄是order

#### View Header處理 ####

    def resizeHeader(self, header):
        '''重整header的大小，主要是給ListView和SearchView用'''
        header.setStretchLastSection(True)
        header.setDefaultSectionSize(200)
        header.setMinimumSectionSize(100)
        header.setResizeMode(QHeaderView.ResizeToContents)
        #別讓欄位小於200，那很難看……
        for index in range(header.count()):
            if not header.isSectionHidden(index):
                if header.sectionSize(index) < 200:
                    header.setResizeMode(index,QHeaderView.Interactive)
                    header.resizeSection(index,200)

## 個別View獨立的操作 ##

#### Tree View操作 ####

    def genTreeView(self):
        '''更新並產生treeView'''
        id,name,order,modtime,cretime,contentLen = self.conn.execute('select id,name,ord,modtime,cretime,len from notes where parentid is NULL').fetchone()

        root = self._buildTreeItem(name, id, order, modtime, cretime, contentLen)
        self._buildTreeModel(root)
        self.treeView.clear()
        self.treeView.addTopLevelItem(root)
        self._sortTree()
        self._expandItem(root, mode = 'tree')

    def _treeClicked(self, item, column):
        '''treeView被點擊'''
        id = int(item.text(1))
        if not self.status['DONT_REFRESH_LISTVIEW']:
            self.genListView(id)
        self.changePage(id)
        #重新定位select
        self.allSelect()

    def _treeClickedByReturn(self, item, column):
        '''基本上和treeClicked相同，不過最後焦點會保持在treeView上'''
        self._treeClicked(item, column)
        self.treeView.setFocus()

    def _treeAfterDrop(self, parentItem):
        '''tree拖放之後，結構改變所以要重新計算結構'''
        for index in range(parentItem.childCount()):
            self.conn.execute('update notes set parentid = :pid, ord = :order where id = :id', {
                'pid':int(parentItem.text(1)),
                'order':index,
                'id':int(parentItem.child(index).text(1))})
        self.conn.commit()

    def _renameTree(self, data):
        '''在TreeView中，執行更名動作'''
        self.conn.execute('update notes set name = ? where id = ?', (data[0] ,data[1]))
        self.conn.commit()
        self.treeView.findItems(data[1], Qt.MatchFixedString | Qt.MatchRecursive, 1)[0].setText(0, data[0])
        try:
            self.listView.findItems(data[1], Qt.MatchFixedString | Qt.MatchRecursive, 1)[0].setText(0, data[0])
        except IndexError: pass
        try:
            self.searchView.findItems(data[1], Qt.MatchFixedString | Qt.MatchRecursive, 1)[0].setText(0, data[0])
        except IndexError: pass
        try:
            self.bookmarkView.findItems(data[1], Qt.MatchFixedString | Qt.MatchRecursive, 1)[0].setText(0, data[0])
        except IndexError: pass
        self._setMainName()
        self.editor.noteArea.setFocus()

    def _sortTree(self):
        '''讓list中的東西依照order順序排列'''
        root = self.treeView.topLevelItem(0)
        self._sortView(root)

#### List View操作 ####

    def genListView(self, id):
        '''更新產生ListView'''
        if id == -1:
            return

        try:
            id,pID = self.conn.execute('select id,parentid from notes where id = ?', (id,)).fetchone()
        except TypeError:
            return

        #開始儲存list折疊狀態
        saveListExpProcess = self.saveListExpandedAsync()

        #如果有pID的話且指定的id沒有子節點，就往更高層產生listView
        rootid = id
        if pID:
            items = self.conn.execute('select id from notes where parentid = ?', (id,)).fetchone()
            if items is None:
                rootid = pID

        #依據rootid建立模型
        id,name,order,modtime,cretime,contentLen = self.conn.execute('select id,name,ord,modtime,cretime,len from notes where id = ?', (rootid,)).fetchone()

        root = self._buildTreeItem(name, id, order, modtime, cretime, contentLen)
        self._buildTreeModel(root)

        #確保listExpanded儲存完畢
        if saveListExpProcess:
            saveListExpProcess.join()

        #重新設定listView
        self.listView.clear()
        self.listView.addTopLevelItem(root)
        self._expandItem(root, mode = 'list')
        self._sortList()

        header = self.listView.header()
        self.resizeHeader(header)

    def _listClicked(self, item, column):
        '''listView被點擊'''
        id = int(item.text(1))
        self.changePage(id)
        #重新定位select
        self.allSelectWithoutTree()

    def _listClickedByReturn(self, item, column):
        '''基本上和listClicked相同，不過最後焦點會保持在listView上'''
        self._listClicked(item, column)
        self.listView.setFocus()

    def genListExpandedDatas(self, item, datas = {}):
        if item == None:
            return datas
        datas[int(item.text(1))] = item.isExpanded()
        for i in range(item.childCount()):
            self.genListExpandedDatas(item.child(i), datas)
        return datas

    def saveListExpandedAsync(self):
        '''非同步儲存ListExpanded資料'''
        listRoot = self.listView.topLevelItem(0)
        if listRoot:
            datas = self.genListExpandedDatas(listRoot)
        else:
            datas = []

        if self.status['MULTIPROCESSING']:
            p = Process(target = subprocess.saveListExpanded,
                    args = (
                        path.cal.bookdb(self.status['BOOKDIR']),
                        datas
                        )
                    )
            p.start()
            return p
        else:
            subprocess.saveListExpanded(
                    bookdb = path.cal.bookdb(self.status['BOOKDIR']),
                    datas = datas)
            return None

    def _sortList(self):
        '''讓list中的東西依照order順序排列'''
        root = self.listView.topLevelItem(0)
        self._sortView(root)

#### 頁面版本記錄（含History View）操作 ####

    def _buildHistoryItem(self, modtime, name, plainLen):
        '''建立一個HistoryItem'''
        newItem = QTreeWidgetItem()
        newItem.setText(0,time.strftime(self.status['TIMEFORMAT_HISTORY'], time.localtime(float(modtime))))
        newItem.setText(1,name)
        newItem.setText(2,str(modtime)) #排序與查找用的時間
        newItem.setText(3,'{:,}'.format(plainLen)) #文字量
        if not self.status['VCS_ENABLE']: #如果VCS沒有啟動，則灰化
            newItem.setFlags(Qt.NoItemFlags)
        return newItem

    def genHistoryView(self, id, modtime = None):
        '''產生history視圖，如果modtime有指定，選到指定的位置'''
        if not modtime: #如果沒有指定modtime，則用原本的modtime來定位
            try:
                modtime = float(self.historyView.currentItem().text(2))
            except AttributeError: pass
        self.historyView.clear()

        datas = self.conn.execute('select modtime,name,len from history where id = ?', (id,)).fetchall()
        for data in datas:
            newItem = self._buildHistoryItem(
                    modtime = data[0],
                    name = data[1],
                    plainLen = data[2])
            self.historyView.addTopLevelItem(newItem)
        self.historyView.sortItems(2, Qt.DescendingOrder)
        self.historyView.header().setResizeMode(0,QHeaderView.ResizeToContents)
        if modtime:
            items = self.historyView.findItems(
                    str(modtime),
                    Qt.MatchFixedString | Qt.MatchRecursive,
                    2)
            try:
                self.historyView.setCurrentItem(items[0])
                self.historyView.scrollToItem(items[0])
            except IndexError: pass

    def _historyClicked(self, item, column):
        '''historyView被點擊'''
        if not self.status['VCS_ENABLE']:
            return
        #準備資料
        self.hPreviewID = self.editor.getID()
        self.hPreviewModtime = item.text(2)
        self.hPreviewHTML = self.getHistory(
                self.hPreviewID,
                self.hPreviewModtime)
        self.ezpage.mainFrame().setHtml(self.hPreviewHTML)
        plain = self.editor.plain #使用plain資料來比較
        #計算差異
        historyPlain = self.ezpage.mainFrame().toPlainText()
        diffs = dmp.diff_main(plain, historyPlain)
        dmp.diff_cleanupSemantic(diffs)
        html = dmp.diff_prettyHtml(diffs)
        self.hPreviewDiff = html.replace('&para;', '') #會插入奇怪的標記，總之去掉
        #計算LenChanged
        lenChanged = len(historyPlain) - len(plain)
        #設定格式
        word = '（{:+,}）'.format(lenChanged)
        if lenChanged > 0: #設定LenChanged格式
            lenChangedStr = '<font color=green>' + word + '</font>'
        elif lenChanged == 0:
            lenChangedStr = '<font color=gray>' + word + '</font>'
        else:
            lenChangedStr = '<font color=red>' + word + '</font>'
        if abs(lenChanged) >= 500:
            lenChangedStr = '<b>' + lenChangedStr + '</b>'

        #if self.status['AUTOBACKUP_LEVENSHTEIN']:
        levenshtein = dmp.diff_levenshtein(diffs)
        ratio = levenshtein / max(len(historyPlain), len(plain))
        worldline = _('World Line')
        word2 = '<big><b><sub>{:} ⇝ {:.2%}</sub></b></big>'.format(worldline, ratio)
        if ratio > 0.30:
            word2 = '<font color=maroon>' + word2 + '</font>'
        lenChangedStr += word2

        #填入title資料到hPreview中
        name = self.conn.execute('select name from notes where id = ?', (self.editor.getID(),)).fetchone()[0]
        title = '{1} → {2} {3}'.format(name, _('Current Version'), item.text(0), lenChangedStr)
        self.hPreviewTitle.setText(title)

        #有必要的話，初始化滾輪位置
        try: self.hPreviewScrollposPlain[self.hPreviewID]
        except: self.hPreviewScrollposPlain[self.hPreviewID] = 0
        try: self.hPreviewScrollposHTML[self.hPreviewID]
        except: self.hPreviewScrollposHTML[self.hPreviewID] = 0
        #記錄滾輪位置後再填入
        pos = self.hPreviewScreen.page().mainFrame().scrollBarValue(Qt.Vertical)
        if self.hPreviewModeHtml.isChecked():
            self.hPreviewScrollposHTML[self.hPreviewID] = pos
            self._setPreviewHtml()
        else:
            self.hPreviewScrollposPlain[self.hPreviewID] = pos
            self._setPreviewPlain()
        #顯示self.hPreview
        self.hPreview.show()
        self.editor.hide()

    def changeHistory(self, id, modtime):
        '''頁面變成指定的history頁面'''
        if id == -1:
            return
        #保存舊頁面, 變更頁面時強制儲存history
        p = self.saveCurrentPageAsync(True)
        if p:
            p.join()

        #更新editor
        data = self.getHistory(id, modtime)
        oldscrollpos = self.editor.scrollpos
        self.editor.inject(htmlstr = data,
                id = id,
                embPath = path.cal.noteemb(self.status['BOOKDIR'], id))
        if self.status['USE_CSS']: #設定 css
            self.editor.csspath = path.cal.notecss(self.status['BOOKDIR'], id, self.status['CSS_FILE'])
        self.editor.scrollpos = oldscrollpos #設定滾頁位置

        self.currentCharcount()

    def saveHistoryAsync(self, id, forceHistoryRecord = False):
        '''以非同步模式進行歷史記錄'''
        #準備資料
        name,modtime,content,plainLen = self.conn.execute('select name,modtime,content,len from notes where id = ?', (id,)).fetchone()
        if self.status['MULTIPROCESSING']:
            p = Process(target = subprocess.saveHistory,
                    args = (
                        self.asyncQueue,
                        path.cal.bookdb(self.status['BOOKDIR']),
                        id,
                        content,
                        modtime,
                        plainLen,
                        name,
                        self.status['VCS_ENABLE'],
                        forceHistoryRecord,
                        self.status['AUTOBACKUP_PERIOD'],
                        self.status['AUTOBACKUP_LENCHANGE'],
                        self.status['AUTOBACKUP_LEVENSHTEIN'])
                    )
            p.start()
            return p
        else:
            subprocess.saveHistory(
                    asyncQueue = self.asyncQueue,
                    bookdb = path.cal.bookdb(self.status['BOOKDIR']),
                    id = id,
                    currentHtml = content,
                    currentModtime = modtime,
                    currentPlainLen = plainLen,
                    currentName = name,
                    vcsEnable = self.status['VCS_ENABLE'],
                    forceHistoryRecord = forceHistoryRecord,
                    autosavePeriod = self.status['AUTOBACKUP_PERIOD'],
                    autosaveLenChanged = self.status['AUTOBACKUP_LENCHANGE'],
                    useLevenshtein = self.status['AUTOBACKUP_LEVENSHTEIN'])
            return None

    def _historyClearClicked(self):
        '''刪除所有本頁所有history'''
        if not dialog.questionDialog(_('Clear all History in this page?'), _('"Wait a moment! It will be clear all history of this page!"\n\nConfirm?'), path.icons.questiondialog):
            return False
        self.conn.execute('delete from history where id = ?',(self.editor.getID(),))
        self.conn.commit()
        self.genHistoryView(self.editor.getID())

    def _historyAddClicked(self):
        '''點擊historyAdd按鈕後發生的事情：強制儲存現在版本'''
        self.saveCurrentPageAsync(True)

    def _hPreviewCancelClicked(self):
        '''hPreviewCancel被點擊，回收資料並把editor重新顯示出來'''
        #記錄pos
        pos = self.hPreviewScreen.page().mainFrame().scrollBarValue(Qt.Vertical)
        if self.hPreviewModeHtml.isChecked():
            self.hPreviewScrollposHTML[self.hPreviewID] = pos
        else:
            self.hPreviewScrollposPlain[self.hPreviewID] = pos
        #重新顯示editor
        self.editor.show()
        self.hPreview.hide()
        #回收資料
        self.hPreviewID = None
        self.hPreviewModtime = None
        self.hPreviewHTML = ''
        self.hPreviewScreen.setPage(None)
        self.hPreviewTitle.setText('')

    def _hPreviewAcceptClicked(self):
        '''hPreviewAccept被點擊，執行套用任務'''
        #執行變更版本任務
        self.changeHistory(self.hPreviewID, self.hPreviewModtime)
        #self.genHistoryView(self.hPreviewID, self.hPreviewModtime)
        #復原編輯器顯示
        self._hPreviewCancelClicked()

    def _hPreviewModeHtmlToggled(self, checked):
        '''進行Preview模式切換動作'''
        if self.hPreviewID == None:
            return
        if checked:
            #記錄滾輪位置
            self.hPreviewScrollposPlain[self.hPreviewID] = self.hPreviewScreen.page().mainFrame().scrollBarValue(Qt.Vertical)
            #填入早已準備好的資料
            self._setPreviewHtml()
        else:
            #記錄滾輪位置
            self.hPreviewScrollposHTML[self.hPreviewID] = self.hPreviewScreen.page().mainFrame().scrollBarValue(Qt.Vertical)
            #填入早已準備好的資料
            self._setPreviewPlain()

######## History的底層操作 ########

    def getHistory(self, id, modtime):
        '''取出指定的歷史記錄content。
        不管底層格式為什麼，自動將其還原回標準的html格式'''
        return subprocess.getHistory(
                bookdb = path.cal.bookdb(self.status['BOOKDIR']),
                id = id,
                modtime = modtime)

    def _setPreviewPlain(self):
        '''以Plain模式填入準備好的資料，並滾動滾輪到變數記錄的位置'''
        self.hPreviewScreen.setHtml(self.hPreviewDiff)
        self.hPreviewScreen.page().mainFrame().setScrollBarValue(Qt.Vertical, self.hPreviewScrollposPlain[self.hPreviewID])
        #復原css與字型格式
        page = self.hPreviewScreen.page()
        page.settings().setUserStyleSheetUrl(QUrl())
        page.settings().resetFontFamily(QWebSettings.StandardFont)
        self.hPreviewScreen.setTextSizeMultiplier(1.0)

    def _setPreviewHtml(self):
        '''以Html模式填入準備好的資料，並滾動滾輪到變數記錄的位置'''
        qpath = QUrl.fromLocalFile(
                path.cal.noteemb(
                    self.status['BOOKDIR'], self.hPreviewID) + '/')
        self.hPreviewScreen.setHtml(self.hPreviewHTML, qpath)
        self.hPreviewScreen.page().mainFrame().setScrollBarValue(Qt.Vertical, self.hPreviewScrollposHTML[self.hPreviewID])
        #加入CSS
        page = self.hPreviewScreen.page()
        if self.status['USE_CSS']:
            cssPath = path.cal.notecss(self.status['BOOKDIR'], self.hPreviewID, self.status['CSS_FILE'])
        else:
            cssPath = ''
        page.settings().setUserStyleSheetUrl(QUrl())
        page.settings().setUserStyleSheetUrl(QUrl.fromLocalFile(cssPath))
        #變更字型
        if self.status['EDITOR_FONT_ST']:
            page.settings().setFontFamily(QWebSettings.StandardFont, self.status['EDITOR_FONT_ST'])
        else:
            page.settings().resetFontFamily(QWebSettings.StandardFont)
        #變更字體乘數
        self.hPreviewScreen.setTextSizeMultiplier(self.status['EDITOR_TEXT_SIZE_MULTIPLIER'])

#### Attach View操作 ####

    def genAttachView(self):
        #更新Att檔案表
        attpath = path.cal.noteatt(self.status['BOOKDIR'],
                self.bhistory.current().editor)
        fModel = QFileSystemModel()
        if os.path.isdir(attpath):
            fModel.setRootPath(attpath)
            self.attachView.setModel(fModel)
            self.attachView.setRootIndex(fModel.index(attpath))
        else:
            self.attachView.setModel(fModel)

    def _attachDoubleClicked(self):
        '''attachView被雙擊'''
        QDesktopServices.openUrl(
                QUrl.fromLocalFile(
                    path.cal.noteatt(
                        self.status['BOOKDIR'],
                        self.bhistory.current().editor)))

    def _attachAddClicked(self):
        '''attach加入按鈕被按下……'''
        filePathList = QFileDialog.getOpenFileNames(
                parent = self,
                caption = _('Select a file you want to insert...'),
                directory = os.getcwd(),
                filter = _('all (*)'));
        if filePathList: #選中後繼續
            noteAttDir = path.cal.noteatt(self.status['BOOKDIR'], self.editor.getID())
            #複製檔案並以相對路徑寫入
            for filePath in filePathList:
                fileName = os.path.split(filePath)[1]
                shuext.copy(filePath, os.path.join(noteAttDir, fileName))
            #更新顯示
            self.genAttachView()

#### Heading View操作 ####

    def genHeadingViewAsync(self):
        '''用當前editor中的資料建立content'''
        if self.status['MULTIPROCESSING']:
            p = Process(target = subprocess.genHeadingItems,
                    args = (
                        self.asyncQueue,
                        self.editor.getID(),
                        self.editor.html
                        )
                    )
            p.start()
            return p
        else:
            subprocess.genHeadingItems(
                    asyncQueue = self.asyncQueue,
                    id = self.editor.getID(),
                    currentHtml = self.editor.html)
            return None

    def applyHeadingItems(self, datas):
        '''用genHeadingViewAsync建立出來的資料包
        （去self.asyncQueue裡找）重建HeadingView'''
        try:
            oldHeading = self.headingView.currentItem().text(0)
            oldIndex = self.headingView.currentItem().text(2)
        except AttributeError:
            oldHeading = None
        self.headingView.clear()

        for data in datas:
            newItem = QTreeWidgetItem()
            newItem.setText(0, data[0]) #資料

            newItem.setText(1, '  ' * data[1] + ' ' + data[0]) #顯示

            newItem.setText(2, str(data[2])) #出現於第幾次
            newItem.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
            self.headingView.addTopLevelItem(newItem)

        #嘗試復原舊資料
        if oldHeading != None:
            items = self.headingView.findItems(oldHeading, Qt.MatchFixedString, 0)
            for i in range(len(items)):
                if items[i].text(2) == oldIndex:
                    self.headingView.setCurrentItem(items[i])
                    self.headingView.scrollToItem(items[i])
                    break

    def _headingClicked(self, item, column):
        '''點擊之後進行滾頁，滾到指定的heading與index'''
        JS = js.scrollToHeading.replace('{$title}', item.text(0))
        JS = JS.replace('{$index}', item.text(2))
        self.editor.noteArea.page().mainFrame().evaluateJavaScript(JS)
        self.editor.noteArea.setFocus()

#### Search（含Search View）操作 ####

    def _searchInput(self, input):
        '''尋找內容'''
        self.searchView.clear()

        keywords = input.split(' ') #用空格拆解內容

        gKey = list() #一般關鍵字包
        titleKey = list() #標題關鍵字包
        attachKey = list() #附件關鍵字包
        imageKey = list() #圖片關鍵字包
        modtimeKey = list() #更動時間關鍵字包
        cretimeKey = list() #創建時間關鍵字包

        #先把混合的keywords分類放好
        for keyword in keywords:
            if keyword == '': #濾掉空字串
                continue
            #檢查每個keyword的前綴字，並將他們分類分開，等後續處理
            if keyword.startswith('t:'): #有「t:」時為「標題關鍵字」
                titleKey.append(keyword[2:])
            elif keyword.startswith('title:'):
                titleKey.append(keyword[6:])
            elif keyword.startswith('a:'): #有「a:」時為「附件關鍵字」
                attachKey.append(keyword[2:])
            elif keyword.startswith('attachment:'):
                attachKey.append(keyword[11:])
            elif keyword.startswith('i:'): #有「i:」時為「圖片關鍵字」
                imageKey.append(keyword[2:])
            elif keyword.startswith('image:'):
                imageKey.append(keyword[6:])
            elif keyword.startswith('m:'): #「m:」為「更動時間關鍵字」
                modtimeKey.append(keyword[2:])
            elif keyword.startswith('modified:'):
                modtimeKey.append(keyword[9:])
            elif keyword.startswith('c:'): #「c:」為「創建時間關鍵字」
                cretimeKey.append(keyword[2:])
            elif keyword.startswith('created:'):
                cretimeKey.append(keyword[8:])
            else:
                gKey.append(keyword)

        #產生集合資料包
        datas = None

        #解析標題包
        for key in titleKey:
            answer = self.conn.execute('select name,id,ord,modtime,cretime,contentplain,len from notes where name like :string', {'string':('%' + key + '%')}).fetchall()
            data = set(answer)
            if datas is None:
                datas = data
            else:
                datas.intersection_update(data)

        #解析通用包
        for key in gKey:
            answer = self.conn.execute('select name,id,ord,modtime,cretime,contentplain,len from notes where contentplain like :string or name like :string', {'string':('%' + key + '%')}).fetchall()
            data = set(answer)
            if datas is None:
                datas = data
            else:
                datas.intersection_update(data)

        #解析附件包
        for key in attachKey:
            if datas is None: #必須要對全部解析
                ids = [data[0] for data in self.conn.execute('select id from notes').fetchall()]
            else:
                ids = [data[1] for data in datas]
            data = set()
            keyUpper = key.upper()
            #將匹配的資料項擷出
            for id in ids:
                attpath = path.cal.noteatt(self.status['BOOKDIR'], id)
                attPattern = os.path.join(attpath, '*')
                for filepath in glob.iglob(attPattern):
                    if keyUpper in os.path.basename(filepath).upper():
                        data.add(self.conn.execute('select name,id,ord,modtime,cretime,contentplain,len from notes where id = ?', (id,)).fetchone())
            if datas is None:
                datas = data
            else:
                datas.intersection_update(data)

        #解析圖片包
        for key in imageKey:
            if datas is None: #必須要對全部解析
                ids = [data[0] for data in self.conn.execute('select id from notes').fetchall()]
            else:
                ids = [data[1] for data in datas]
            data = set()
            keyUpper = key.upper()
            #將匹配的資料項擷出
            for id in ids:
                embpath = path.cal.noteemb(self.status['BOOKDIR'], id)
                embPattern = os.path.join(embpath, '*')
                for filepath in glob.iglob(embPattern):
                    if keyUpper in os.path.basename(filepath).upper():
                        data.add(self.conn.execute('select name,id,ord,modtime,cretime,contentplain,len from notes where id = ?', (id,)).fetchone())
            if datas is None:
                datas = data
            else:
                datas.intersection_update(data)

        #解析修改時間包
        for key in modtimeKey:
            timerange = timestr.getTimeRangeByString(key)
            #print(time.ctime(timerange[0]), time.ctime(timerange[1]))
            if timerange:
                answer = self.conn.execute('select name,id,ord,modtime,cretime,contentplain,len from notes where modtime > ? and modtime < ?', (timerange[0], timerange[1])).fetchall()
                data = set(answer)
                if datas is None:
                    datas = data
                else:
                    datas.intersection_update(data)

        #解析創建時間包
        for key in cretimeKey:
            timerange = timestr.getTimeRangeByString(key)
            if timerange:
                answer = self.conn.execute('select name,id,ord,modtime,cretime,contentplain,len from notes where cretime > ? and cretime < ?', (timerange[0], timerange[1])).fetchall()
                data = set(answer)
                if datas is None:
                    datas = data
                else:
                    datas.intersection_update(data)

        #如果完全沒有資料就放棄
        if datas is None:
            self.searchNumber.setText('>>>>')
            self.editor.findtext = ''
            return

        #資料包產生完畢，進行排序
        dataHeap = []
        for data in datas:
            hitNumber = 0
            #依據查詢的關鍵字在文中出現次數判斷關聯，進行排序
            for key in gKey:
                if keyword == '':
                    continue
                hitNumber += data[5].lower().count(keyword.lower())
            heapq.heappush(dataHeap, (hitNumber, data))

        datas = [heapq.heappop(dataHeap)[1] for i in range(len(dataHeap))]
        datas.reverse()

        #更新hitNumber
        if len(datas) == 0:
            displayText = '<font size = 5 color = red><i><b>0</b></i></font>'
        elif len(datas) <= 5:
            displayText = '<font size = 5><i><b>' + str(len(datas)) + '</b></i></font>'
        elif len(datas) <= 20:
            displayText = '<font size = 4><i><b>' + str(len(datas)) + '</b></i></font>'
        else:
            displayText = '<i><b>' + str(len(datas)) + '</b></i>'
        self.searchNumber.setText(displayText)

        #產生顯示物件
        for data in datas:
            item = self._buildTreeItem(data[0], data[1], data[2], data[3], data[4], data[6])
            item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable)
            self.searchView.addTopLevelItem(item)
        #重整欄位大小
        header = self.searchView.header()
        self.resizeHeader(header)

        #最後將第一個通用keyword填入editor的searchBar中，方便搜尋
        if len(gKey) > 0:
            self.editor.findtext = gKey[0]
        else:
            self.editor.findtext = ''

    def _searchClicked(self, item, column):
        '''searchView被點擊'''
        #定位treeView
        treeItem = self.treeView.findItems(item.text(1), Qt.MatchFixedString | Qt.MatchRecursive, 1)[0]
        self.treeView.setCurrentItem(treeItem)
        #定位bookmarkView
        try:
            bookmarkItem = self.bookmarkView.findItems(item.text(1), Qt.MatchFixedString | Qt.MatchRecursive, 1)[0]
            self.bookmarkView.setCurrentItem(bookmarkItem)
        except IndexError:
            pass
        #載入頁面
        #如果listView中有目標，直接點取之，如果沒有則更新listView
        items = self.listView.findItems(
                item.text(1),
                Qt.MatchFixedString | Qt.MatchRecursive,
                1)
        if len(items) > 0:
            self.listView.setCurrentItem(items[0])
            self._listClicked(items[0], column)
        else:
            self._treeClicked(item, column)

    def _searchClickedByReturn(self, item, column):
        '''基本上和searchClicked相同，不過最後焦點會保持在searchView上'''
        self._searchClicked(item, column)
        self.searchView.setFocus()

    def searchHotKeyPressed(self):
        '''當按下搜尋快速鍵時執行這個'''
        if self.searchBar.hasFocus(): #已經在搜尋的狀態下，翻轉檢視器
            self.tabup.setCurrentIndex(0)
        else: #否則直接對焦
            self.tabup.setCurrentIndex(1)
            self._tabupChanged(1)
            #將選擇區（如果存在）內容移過來
            #selStr = self.editor.noteArea.page().selectedText()
            #if selStr != '':
            #    self.searchBar.setText(selStr)

#### Bookmark（含Bookmark View）操作 ####

    def genBookmarkView(self):
        '''產生bookmarkView'''
        self.bookmarkView.clear()
        answers = self.conn.execute('select name,id,bookmark from notes where bookmark is not null order by bookmark').fetchall()
        for answer in answers:
            item = self._buildTreeItem(answer[0],answer[1],0,0,0,0)
            self.bookmarkView.addTopLevelItem(item)

    def _bookmarkAddClicked(self):
        '''點擊bookmarkAdd按鈕後發生的事情'''
        newBookmarkOrder = self.bookmarkView.topLevelItemCount()
        self.conn.execute(
                'update notes set bookmark = ? \
                        where id = ? and bookmark is null',
                        (newBookmarkOrder, self.bhistory.current().editor))
        self.conn.commit()
        self.genBookmarkView()

    def _bookmarkDeleteClicked(self):
        '''點擊bookmarkDelete按鈕後發生的事情'''
        id = self.bhistory.current().editor
        self.conn.execute(
                'update notes set bookmark = null where id = ?', (id,))
        #排序
        answers = self.conn.execute(
                'select id from notes \
                        where bookmark is not null order by bookmark'
                        ).fetchall()
        for index in range(len(answers)):
            self.conn.execute(
                    'update notes set bookmark = ? where id = ?',
                    (index,answers[index][0]))
        self.conn.commit()
        #重新產生列表
        self.genBookmarkView()

    def _bookmarkClearClicked(self):
        '''刪除所有bookmark'''
        if not dialog.questionDialog(_('Clear all bookmark?'), _('"It will be clear all your bookmarks..."\n\nConfirm?'), path.icons.questiondialog):
            return False
        self.conn.execute('update notes set bookmark = null')
        self.conn.commit()
        self.genBookmarkView()

    def _bookmarkClickedByReturn(self, item, column):
        '''基本上和searchClicked相同，不過最後焦點會保持在bookmarkView上'''
        self._searchClicked(item, column)
        self.bookmarkView.setFocus()

    def _bookmarkAfterDrop(self, currentItem):
        '''bookmark拖放之後，重新登記順序'''
        for index in range(self.bookmarkView.topLevelItemCount()):
            self.conn.execute('update notes set bookmark = :bookmark where id = :id', {
                'bookmark':index,
                'id':int(self.bookmarkView.topLevelItem(index).text(1))})
        self.conn.commit()

#工具列的結構按鈕

#### 結構按鈕的後端操作 ####

    def createANewNoteInDB(self, conn, name, pID, insertPos, content):
        '''在資料庫中建立一個新筆記內容
        回傳新建立的筆記ID'''
        cretime = time.mktime(time.localtime())
        lastrowid = conn.execute(
                'insert into notes(\
                        name, parentid, cretime, modtime, tree_expanded, list_expanded, ord, content, contentplain, len, uuid)\
                        values(?,?,?,?,?,?,?,?,?,?,?)',
                        (name, pID, cretime, cretime, True, True, insertPos, content, '', 0, str(uuid.uuid4()))).lastrowid
        cID = conn.execute('select id from notes where rowid = ?', (lastrowid,)).fetchone()[0]
        conn.commit()
        return cID

#### 一級按鈕 ####

    def newNote(self):
        '''建立一個同一層級的新節點'''
        cID = self.bhistory.current().editor
        pID,order = self.conn.execute(
                'select parentid,ord from notes where id = ?',
                (cID,)).fetchone()
        if pID == None: #在根結點使用newNote，等效於使用newSubNote
            self.newSubNote(cID)
            return
        self.newSubNote(pID, order+1)

    def newSubNote(self, pID = None, insertPos = -1):
        '''依據p(erent)ID建立一個新的子節點'''
        if not pID:
            pID = self.bhistory.current().editor

        #尋找可用的臨時名字
        name = _('Unnamed Note')
        index = 1
        while self.conn.execute(
                'select name from notes where parentid = ? and name = ?',
                (pID, name + str(index))).fetchone() != None:
            index += 1
        else:
            name += str(index)

        #排序的同時插入
        heap = []
        datas = self.conn.execute(
                'select ord,id from notes where parentid = ?',
                (pID,)).fetchall()
        for data in datas:
            heapq.heappush(heap, (int(data[0]),int(data[1])))
        if insertPos == -1:
            insertPos = len(heap)
        for i in range(len(heap) + 1):
            if i == insertPos: #建立
                cID = self.createANewNoteInDB(
                        conn = self.conn,
                        name = name,
                        pID = pID,
                        insertPos = i,
                        content = '<!DOCTYPE html><html><head></head><body></body></html>')
                continue
            self.conn.execute(
                    'update notes set ord = ? where id = ?',
                    (i, heapq.heappop(heap)[1]))
            self.conn.commit()

        #TreeView的更新，透過id找parent
        cretime = self.conn.execute(
                'select cretime from notes where id = ?',
                (cID,)).fetchone()[0]
        pItem = self.treeView.findItems(str(pID) ,Qt.MatchRecursive, 1)[0]
        cItem = self._buildTreeItem(name, cID, insertPos, cretime, cretime,0)
        pItem.insertChild(insertPos, cItem)

        #更新整體View
        st = self.bhistory.current()
        self.genListView(st.treeID)
        self.changeBrowsing(status.bstatus(st.treeID, cID, cID))
        #更名
        self.treeView.setCurrentItem(cItem)
        self.treeView.editItem(cItem, 0)

    def delNote(self):
        '''刪除當前筆記頁，與以下所有的子筆記頁'''
        def recuDelSubNote(id):
            '''刪除id與他以下的所有筆記'''
            try:
                shutil.rmtree(path.cal.notedir(self.status['BOOKDIR'], id))
            except OSError: pass
            self.conn.execute('delete from notes where id = ?',(id,))
            self.conn.execute('delete from history where id = ?',(id,))
            self.conn.commit()
            subids = self.conn.execute('select id from notes where parentid = ?',(id,)).fetchall()
            for subid in subids:
                recuDelSubNote(subid[0])

        st = self.bhistory.current()
        pID,name = self.conn.execute(
                'select parentid,name from notes where id = ?',
                (st.editor,)).fetchone()
        if pID == None:
            return False

        if not dialog.questionDialog(_('Delete notepage'),
                _('<i>"This action may have some risk..."</i><br><br>Are you sure to delete this page:<br><br><b>"{0}"</b> ?').format(name),
                path.icons.questiondialog):
            return False

        st = self.bhistory.current()
        pID = self.conn.execute(
                'select parentid from notes where id = ?',
                (st.editor,)).fetchone()[0]
        #tree部份
        Item = self.treeView.findItems(str(st.editor) ,Qt.MatchRecursive, 1)[0]
        pItem = self.treeView.findItems(str(pID) ,Qt.MatchRecursive, 1)[0]
        pItem.removeChild(Item)
        #資料庫部份，同時刪除history與資料夾
        recuDelSubNote(st.editor)
        #轉移焦點到上一步
        self.changeBrowsing(self.bhistory.before())

    def noteBook(self):
        '''叫出notebook選單'''
        self.notebookMenu.exec_(QCursor.pos() + QPoint(0,-10), self._noteBook)

    def about(self):
        '''讓使用者檢視說明視窗'''
        if not self.isVisible():
            self.displayChange()

        msgBox = QMessageBox(QMessageBox.NoIcon,
                _('about LoNote') + ' ' + version.version,
                info.LoNoteInfo,
                QMessageBox.Ok,
                self,
                Qt.Dialog|Qt.MSWindowsFixedSizeDialogHint )
        msgBox.setDetailedText(info.SystemInfo)
        #msgBox.setDetailedText('detail')
        msgBox.exec()

    def cfgDialog(self):
        '''叫出設定視窗讓使用者做設定'''
        if not self.isVisible():
            self.displayChange()

        self.cDialog.reSet(self.status)
        self.cDialog.show()

#### Notebook按鈕的次級按鈕 ####

    def newNoteBook(self):
        '''呼叫視窗，讓使用者新建一個筆記簿'''
        #讓使用者選擇路徑
        newPath = QFileDialog.getSaveFileName(self,
                caption = _('Select a empty folder for your new notebook'),
                filter = _('folder'))
        if not newPath:
            return False
        os.mkdir(newPath)

        dba.tableinit(path.cal.bookdb(newPath))
        dba.checkUpgrade(path.cal.bookdb(newPath))
        conn = sqlite3.connect(path.cal.bookdb(newPath))
        #把root建立好
        self.createANewNoteInDB(
                conn = conn,
                name = _('Unnamed Notebook'),
                pID = None,
                insertPos = 0,
                content =
                '<!DOCTYPE html><html><head></head><body>' +
                _('this is your new notebook\'s cover page.') +
                '</body></html>')
        conn.commit()
        conn.close()
        self.closeBook()
        self.status['ISEXAMPLE'] = False
        self.status['BOOKDIR'] = newPath
        self.loadBook()

    def loadNoteBook(self, tPath = ''):
        '''載入NoteBook資料夾
        如果沒有指定，叫出對話框讓使用者選擇
        成功回傳True，否則回傳False'''
        #如果沒有輸入目錄，就讓使用者先用對話盒選擇
        if not tPath:
            tPath = QFileDialog.getExistingDirectory(self, caption = _('select notebook folder'))
            if not os.path.isdir(tPath): #選擇後仍然不存在就放棄
                return False

        #檢測是否為舊版筆記
        if os.path.isfile(os.path.join(tPath,'notebook.xml')) and os.path.isfile(os.path.join(tPath,'node.xml')) and os.path.isfile(os.path.join(tPath,'page.html')):
            dialog.infoDialog(_('Convert a old notebook'),
                _('''"Look like a old version notebook?"\n\n"{0}" is a old notebook.\n\nIf you want to update this notebook, Using configure button to convert it.''').format(tPath),path.icons.questiondialog)
            return False

        #檢測筆記路徑下是否有db檔
        if not os.path.isfile(path.cal.bookdb(tPath)):
            return False

        self.closeBook()
        self.status['BOOKDIR'] = tPath
        self.status['ISEXAMPLE'] = False
        self.loadBook()

    def fastExportToHtml(self):
        '''快速Export本頁為Html格式'''
        #先儲存當前版本
        p = self.saveCurrentPageAsync()
        if p: #使用同步模式！
            p.join()
        #匯出
        exportFile = self.exportToHtml(self.editor.getID() ,recu = False)
        #自動開啟供使用者檢視
        if self.status['OPEN_AFTER_EXPORT_TO_HTML'] == True:
            QDesktopServices.openUrl(QUrl.fromLocalFile(exportFile))

    def exportToHtmls(self):
        '''匯出當前頁面與所有的子頁面為一個網站'''
        #先儲存當前版本
        p = self.saveCurrentPageAsync()
        if p: #使用同步模式！
            p.join()
        #匯出
        exportFile = self.exportToHtml(self.editor.getID() ,recu = True)
        #自動開啟供使用者檢視
        if self.status['OPEN_AFTER_EXPORT_TO_HTML'] == True:
            QDesktopServices.openUrl(QUrl.fromLocalFile(exportFile))

    def exportToNewNotebook(self):
        '''Export本頁和他的子頁為新的Notebook'''
        #先儲存一次，確保資料庫的資料是最新狀態
        p = self.saveCurrentPageAsync()
        if p:
            p.join()
        #收集共用資料
        newBookName = self.conn.execute('select name from notes where id = ?', (self.editor.getID(),)).fetchone()[0]
        #替換不合法的bookname字元
        newBookName = newBookName.translate(str.maketrans(' :*<>#?/|\\"','_：＊＜＞＃？／｜＼”'))
        exportDir = path.cal.exportdir(self.status['BOOKDIR'])
        newBookDir = os.path.join(exportDir, newBookName)

        #收集所有需要匯出的子頁面的訊息
        data = self.dataMapping(id = self.editor.getID(),
                sourceBookDir = self.status['BOOKDIR'],
                targetBookDir = newBookDir,
                idcounter = 1)
        #檢測當前筆記資料項的長度，拼裝SQL語句：
        notesIDPos, notesPIDPos, historyIDPos, SQL_notesInsertString, SQL_historyInsertString, notesOrdPos = self.getDBLineInfo(self.status['BOOKDIR'])

        #建立新筆記簿
        #先檢測路徑是否存在
        if not os.path.isdir(exportDir):
            os.mkdir(exportDir)
        if os.path.isdir(newBookDir):
            shutil.rmtree(newBookDir)
        os.mkdir(newBookDir)
        #建立並連結新的sqlite資料庫
        dba.tableinit(path.cal.bookdb(newBookDir))
        dba.checkUpgrade(path.cal.bookdb(newBookDir))
        conn = sqlite3.connect(path.cal.bookdb(newBookDir))

        #複製所有資料並mapping轉化id與parentId
        for id in data:
            row = list(self.conn.execute('select * from notes where id = ?', (id,)).fetchone())
            row[notesIDPos] = data[id]['newid'] #mapping newID
            #mapping newpID
            if data[id]['newid'] == 1:
                #根節點的parentID不再需要
                row[notesPIDPos] = None
                #根結點的Ord設為0
                row[notesOrdPos] = 0
            else:
                row[notesPIDPos] = data[row[notesPIDPos]]['newid']
            conn.execute(SQL_notesInsertString, row)
            #處理History
            for historyRow in self.conn.execute('select * from history where id = ?', (id,)).fetchall():
                historyRow = list(historyRow)
                historyRow[historyIDPos] = data[id]['newid']
                conn.execute(SQL_historyInsertString, historyRow)
            #複製附加資料
            if os.path.isdir(data[id]['notedir']):
                shutil.copytree(data[id]['notedir'], data[id]['newnotedir'])
        conn.commit()
        conn.close()

    def importFromNotebook(self, tPath = ''):
        '''將一本外來筆記簿Import為當前筆記頁的子頁'''
        #先選擇路徑
        if not tPath:
            tPath = QFileDialog.getExistingDirectory(self, caption = _('select import notebook\'s folder'))
            if not os.path.isdir(tPath) or not os.path.isfile(path.cal.bookdb(tPath)): #選擇後仍然不存在就放棄
                return False
        #先儲存一次，確保資料庫的資料是最新狀態
        p = self.saveCurrentPageAsync()
        if p:
            p.join()
        #替目標資料庫升級
        dba.checkUpgrade(path.cal.bookdb(tPath))
        #收集映射表與附加資料
        data = self.dataMapping(id = 1,
                sourceBookDir = tPath,
                targetBookDir = self.status['BOOKDIR'])
        #檢測當前筆記資料項的長度，拼裝SQL語句：
        notesIDPos, notesPIDPos, historyIDPos, SQL_notesInsertString, SQL_historyInsertString, notesOrdPos = self.getDBLineInfo(self.status['BOOKDIR'])
        #建立連線
        conn = sqlite3.connect(path.cal.bookdb(tPath))
        #將conn的所有資料複製到self.conn中
        for id in data:
            row = list(conn.execute('select * from notes where id = ?', (id,)).fetchone())
            row[notesIDPos] = data[id]['newid'] #mapping newID
            #mapping newpID
            if data[id]['id'] == 1:
                #根結點的Ord設為最下面
                ans = self.conn.execute('select Ord from notes where parentid = ? order by Ord desc', (self.editor.getID(),)).fetchone()
                if ans is None:
                    row[notesOrdPos] = 0
                else:
                    row[notesOrdPos] = ans[0] + 1
                #根節點的parentID設為新的值
                row[notesPIDPos] = self.editor.getID()
            else:
                row[notesPIDPos] = data[row[notesPIDPos]]['newid']
            self.conn.execute(SQL_notesInsertString, row)
            #處理History
            for historyRow in conn.execute('select * from history where id = ?', (id,)).fetchall():
                historyRow = list(historyRow)
                historyRow[historyIDPos] = data[id]['newid']
                self.conn.execute(SQL_historyInsertString, historyRow)
            #複製附加資料
            if os.path.isdir(data[id]['notedir']):
                shutil.copytree(data[id]['notedir'], data[id]['newnotedir'])
        #關閉
        conn.close()
        self.conn.commit()
        #更新檢視器
        self.genTreeView()
        self.genListView(self.editor.getID())
        self.changeBrowsing(self.bhistory.current())

#### 按鈕產生函式 ####

    def genNotebookMenu(self):
        '''拼裝notebookMenu'''
        def genload(bookpath):
            '''產生載入任務的函式'''
            def load():
                if os.path.isdir(bookpath) and os.path.isfile(path.cal.bookdb(bookpath)):
                    self.closeBook()
                    self.status['ISEXAMPLE'] = False
                    self.status['BOOKDIR'] = bookpath
                    self.loadBook()
            return load

        def loadExample():
            '''載入Example'''
            self.closeBook()
            self.status['ISEXAMPLE'] = True
            self.loadBook()

        self.notebookMenu.clear()
        #加入之前讀取過的筆記
        for i in range(len(self.status['NOTEBOOK_HISTORY'])):
            name = self.status['NOTEBOOK_HISTORY'][i][0]
            bookpath = self.status['NOTEBOOK_HISTORY'][i][1]
            #如果路徑依舊存在才加入
            if os.path.isdir(bookpath) and os.path.isfile(path.cal.bookdb(bookpath)):
                action = QAction(_('Open: ') + name, self)
                action.setStatusTip(_('Open a notebook from: "{0}"').format(bookpath))
                action.triggered.connect(genload(bookpath))
                self.notebookMenu.addAction(action)
        self.notebookMenu.addSeparator()
        #加入讀取筆記本
        self.notebookMenu.addAction(self._loadNoteBook)
        #加入範例筆記本
        action = QAction(QIcon(path.icons.loadnotebook), _('Load Example'), self)
        action.setStatusTip(_('Load the example notebook.'))
        action.triggered.connect(loadExample)
        #action.setIconVisibleInMenu(True)
        self.notebookMenu.addAction(action)
        self.notebookMenu.addSeparator()
        #加入匯出功能
        self.notebookMenu.addAction(self._fastExportToHtml)
        self.notebookMenu.addAction(self._exportToHtmls)
        self.notebookMenu.addAction(self._exportToNewNotebook)
        self.notebookMenu.addSeparator()
        #加入匯入功能
        self.notebookMenu.addAction(self._importFromNotebook)
        self.notebookMenu.addSeparator()
        #加入新建筆記本
        self.notebookMenu.addAction(self._newNoteBook)

#HTML大量匯出時用的內容
CSS_HTMLMultiExport = '''\
html, body, div, iframe { margin:0px; padding: 0px; height: 100%; }
iframe { display: block; width: 100%; border: none; }

body{
margin: auto;
margin-left:auto;
margin-right:auto;
width:95%;
}

aside#nav{
padding:0px 0px 0px 0px;
margin: 5px;
background:#efefef;
border:#CCCCCC 1px solid;
border-radius:3px;
-moz-border-radius:3px;
-khtml-border-radius:3px;
width:20%;
max-width: 250px;
float:left;
height:95%;
}

iframe#nav{
border:#CCCCCC 1px none;
border-radius:3px;
-moz-border-radius:3px;
-khtml-border-radius:3px;
width:100%;
height: 100%;
}

div#main{
height:95%;
}

iframe#main{
padding: 15px;
margin: 10px;
background:#f8f8f8;
border:#CCCCCC 1px solid;
border-radius:3px;
-moz-border-radius:3px;
-khtml-border-radius:3px;
width: 70%;
height: 95%;
}
'''
